Criando campos data e calculado em tempo de execução
Essa semana tive que implementar uma tela que mostrasse um grid com a listagem de produtos, e ao lado uma coluna para cada tabela de preço com seu respectivo preço, além de uma coluna que tivesse um campo InternalCalc booleano para que fosse possível selecionar os produtos através de um checkbox no meu grid (utilizo o cxGrid da DevExpress, então os campos booleanos podem ser exibidos com um checkbox na coluna).
O drama começa na geração do Sql. Como eu posso ter quantas tabelas de preço eu quiser, logo as colunas deverão ser dinâmicas, cada tabela deverá gerar um campo novo no meu select. Eu estou utilizando Firebird, então não tenho nenhum recurso de Olap ou Pivot, então meu Sql teve que ser gerado por um método no Delphi, onde eu faço um loop para cada uma das tabelas de preço gerar uma coluna coluna correspondente, isso ficou mais ou menos assim (mudei a nomenclatura dos meus campos para ficar mais didático):
procedure TdmdCadAlteraPreco.MontarConsulta;
var
lSql : TStringList;
lCod, lCampo : String;
lField : TField;
begin
lSql := TStringList.Create;
try
lSql.Add('SELECT P.PROD_ID, P.PROD_CODIGO, P.PROD_DESC ');
cdsTabPreco.Close;
cdsTabPreco.Open;
cdsTabPreco.First;
while not cdsTabPreco.Eof do
begin
lCod := IntToStr(cdsTabPreco.FieldByName('TPRE_ID').AsInteger);
lCampo := 'TABELAPRECO_'+lCod;
lSql.Add(',(SELECT F.PRECO FROM PRECOPRODUTO F ');
lSql.Add(' WHERE F.PROD_ID = P.PROD_ID ');
lSql.Add(' AND F.TPRE_ID = '+lCod+') as '+ lCampo);
cdsTabPreco.Next;
end;
cdsTabPreco.Close;
lSql.Add('FROM PRODUTO P');
sqlPesquisa.CommandText := lSql.Text;
end;
Como meu velho pai costumava dizer, "para bom programador meio código basta". Então, a unica coisa que não está visível aqui é o Sql do cdsTabPreco, que tem a lista das tabelas de preço cadastradas no sistema. Gerar Sql geralmente não é mistério pra ninguém, o problema está em colocar estes campos no CDS de pesquisa, para que possamos usar por exemplo o DisplayFormat, o DisplayName e também adicionar campos InteralCal.
Confesso que até o momento nunca tinha passado por tal situação, minhas Sqls são todas colocadas diretamente nos datasets, e tenho um framework de pesquisa que apenas trabalha o where das Sqls, as colunas nunca são alteradas.
Na primeira tentativa fui direto para o FieldDefs do CdsPesquisa, já que estou acostumado a criar CDSs temporários utilizando esses comandos:
cdsPesquisa.FieldDefs.Add('PROD_ID',ftInteger);
cdsPesquisa.FieldDefs.Add('PROD_CODIGO',ftString,15);
...
Essa técnica foi frustrante, por que eu não tenho aqui como dizer se meu campo é calculado ou não.
A próxima lógica ao meu ver seria criar um objeto do tipo TField e adiciona-lo ao CDS. Algo dessa forma:
lField := TIntegerField.Create(Self);
lField.FieldName := 'PROD_ID';
lField.FieldKind := fkData;
cdsPesquisa.Fields.Add(lField);
Aqui eu consigo definir o tipo do campo, se é data, calculado ou InternalCal, mas na hora de abrir o CDS...
Não é que o "chato do pato", ou melhor do CDS ficou reclamando que o campo não existia no dataset!
Ai fiquei matutando, qual será a mandinga para adicionar meu campo no CDS?
Foi então que dando um pesquisada por ai descubro que não devemos adicionar o TField a lista de TFields do CDS, e sim atribuir para a propriedade DataSet do TField o DataSet onde ele será adicionado. Dessa forma:
lField := TIntegerField.Create(Self);
lField.FieldName := 'PROD_ID';
lField.FieldKind := fkData;
lField.DataSet := cdsPesquisa;
Facil né?
Claro, depois que sabemos o que fazer.
Nosso código completo fica assim:
procedure TdmdCadAlteraPreco.MontarConsulta;
var
lSql : TStringList;
lCod, lCampo : String;
lField : TField;
begin
lSql := TStringList.Create;
try
lSql.Add('SELECT P.PROD_ID, P.PROD_CODIGO, P.PROD_DESC ');
lField := TBooleanField.Create(Self);
lField.FieldName := 'SELECAO';
lField.FieldKind := fkInternalCalc;
lField.DisplayLabel := 'Sel';
lField.DataSet := cdsPesquisa;
lField := TIntegerField.Create(Self);
lField.FieldName := 'PROD_ID';
lField.FieldKind := fkData;
lField.Visible := False;
lField.DataSet := cdsPesquisa;
lField := TStringField.Create(Self);
lField.FieldName := 'PROD_CODIGO';
lField.FieldKind := fkData;
lField.Size := 15;
lField.DisplayLabel := 'Código';
lField.DataSet := cdsPesquisa;
lField := TStringField.Create(Self);
lField.FieldName := 'PROD_DESC';
lField.FieldKind := fkData;
lField.Size := 50;
lField.DisplayLabel := 'Descrição';
lField.DataSet := cdsPesquisa;
cdsTabPreco.Close;
cdsTabPreco.Open;
cdsTabPreco.First;
while not cdsTabPreco.Eof do
begin
lCod := IntToStr(cdsTabPreco.FieldByName('TPRE_ID').AsInteger);
lCampo := 'TABELAPRECO_'+lCod;
lSql.Add(',(SELECT F.PRECO FROM PRECOPRODUTO F ');
lSql.Add(' WHERE F.PROD_ID = P.PROD_ID ');
lSql.Add(' AND F.TPRE_ID = '+lCod+') as '+ lCampo);
lField := TFMTBCDField.Create(Self);
lField.FieldName := lCampo;
lField.FieldKind := fkData;
lField.DataSet := cdsPesquisa;
TFMTBCDField(lField).DisplayFormat := '0.,00';
lField.DisplayLabel :=
cdsTabPreco.FieldByName('TPRE_DESC').AsString;
cdsTabPreco.Next;
end;
cdsTabPreco.Close;
lSql.Add('FROM PRODUTO P');
sqlPesquisa.CommandText := lSql.Text;
finally
lSql.Free;
end;
end;
Observe que cada Field e instanciado usando a classe correspondente ao seu tipo no banco, TIntegerField, TFMTBCDField, TStringField ...
Galerinha, fica a dica e até a próxima!