Eis que estou de volta! E agora vai ser bem complicado arrumar uma desculpa pela demora… portanto, me perdoem. É o que me resta a fazer.
No recesso do Carnaval, após um tempo sem olhar o blog, aproveitei para dar uma atualizada em seus plugins. Ao acessar a área de administração, me deparei com vários pedidos relacionados à nossa série de ORM. Mais especificamente sobre a questão: Relacionamentos.
Até hoje eu particularmente não senti falta deste recurso, visto que a princípio meu maior desejo era acabar com o uso – interminável! – dos métodos e parâmetros das querys (ParamByName, FieldByName, AsInteger, AsString, AsDateTime…). Além disso, queria abstrair o máximo possível o processo de CRUD no meu sistema, evitando assim ficar dependente de componentes de conexão como o IBX, UIB, FireDac, etc.
Creio que para os objetivos citados, o nosso projeto já cumpre muito bem o seu papel, de forma simples e leve. Mas como vocês têm me pedido, vou iniciar o trato do relacionamento.
Será interessante acessar os fontes do projeto para ir acompanhando, visto que apenas colocarei trechos dos códigos e não a unit completa. Além disso, neste post me aterei, no que se refere a suíte de componentes, a unit que trata do FireDac, ou seja, Lca.Orm.FireDac. Cabendo a você repassar para a sua suíte o que for demonstrado aqui.
Fontes atualizados estão em: https://github.com/luizsistemas/ORM-Basico-Delphi
Relacionamento 1:N e Tabelas Exemplo
Não é o foco abordar neste post a normalização de tabelas e cardinalidade. O objetivo será única e exclusivamente definir que tipo de relacionamento iremos trabalhar.
Afim de dar um insight, um direcionamento a quem pretende aventurar-se por estas águas, eu vou mostrar uma alternativa de como implementar o processamento do relacionamento cuja a cardinalidade seja 1:N.
Para os nossos testes, utilizaremos o exemplo de um funcionário, que se encontra em uma cidade e está lotado em um departamento. O funcionário está no lado N (vários), visto que UM departamento pode ter 0, 1 ou VÁRIOS funcionários (vá entender o porquê de um departamento não ter ninguém, mas…). Da mesma forma, UMA cidade por ter 0, 1 ou VÁRIOS funcionários.
No Github do nosso projeto, na pasta teste/bd, contém o script de criação destas tabelas, ou seja, tabela FUNCIONARIO e DEPTO (a tabela Cidade já havia sido criada no início da série) e seus relacionamentos.
Refatorar é preciso
Teremos de fazer alguns ajustes para, aí sim, partir para a nova implementação.
As razões para isso se deve ao fato que antes, somente um comando era executado por vez por requisição. Por isso temos um objeto chamado FQuery em TDaoFireDac, que é global da classe, para execução de comandos SQL (inserts, updates e deletes).
Porém, este cenário deverá ser alterado, visto que poderão ocorrer várias chamadas a este objeto numa mesma requisição. Por exemplo, ao alterar funcionário, poderemos também inserir ou alterar um departamento. O mesmo vale para cidade. Sendo o FQuery global, inevitavelmente haverão conflitos, visto que um mesmo objeto estará recebendo várias instruções SQL concomitantemente numa mesma requisição.
Portanto, devemos mudar a forma inicial pensada para o FQuery. Este não mais poderá ser uma variável global. E fazendo uma análise bem rápida, percebemos também que nós temos métodos que constroem querys, ou seja, os métodos GetID, GetMax e GetRecordCount, cujos objetos deveriam ser destruídos dentro do escopo do método, e não é isso que acontece atualmente. Eles continuam em memória até a finalização da aplicação por conta do parâmetro, “Application”, passado na criação de cada objeto query.
Ajustes finos
Devemos primeiro, renomear a interface IDaoBase para IDaoBaseComandos, em Lca.Orm.Base. E a classe TQueryFireDac, para TParamsFireDac.
Além disso, dando uma passada pelas units que compõem o projeto, iremos retirar comentários desnecessários do código e corrigir o nome de algumas variáveis e métodos (CamelCase e padronização). Ajustes finos que melhoram o entendimento e leitura do código. Acompanhe pelo fontes para visualizar todas as pequenas e importantes alterações efetuadas.
Nova Interface e nova Classe: IQuery e TQueryFD
Vamos criar em Lca.Orm.Base a interface IQuery:
IQuery = interface ['{52E7E2A0-C3E7-41FC-86B1-50A50220C474}'] function Sql: TStrings; function Dataset: TDataset; function RowsAffected: Integer; function RecordCount: Integer; procedure Executar; procedure Abrir; end;
Essa interface terá a responsabilidade de resolver o problema de múltiplas requisições SQL gerarem conflitos, além de evitar que os objetos criados nos métodos que constroem querys (GetId, GetMax, GetRecordCount, ConsultaSql, ConsultaAll, etc.) ficassem em memória até o fechamento da aplicação.
Partiremos para a implementação da classe concreta TQueryFD, Lca.Orm.Comp.FireDac:
... TQueryFD = class(TInterfacedObject, IQuery) private FQuery: TFDQuery; FSql: TStrings; function RecordCount: Integer; procedure Abrir; public constructor Create(Conexao: TFDConnection; Transacao: TFDTransaction); destructor Destroy; override; function Sql: TStrings; function Dataset: TDataset; function RowsAffected: Integer; procedure Executar; end; ... implementation ... { TQueryFireDac } constructor TQueryFD.Create(Conexao: TFDConnection; Transacao: TFDTransaction); begin FQuery := TFDQuery.Create(nil); FQuery.Connection := Conexao; FQuery.Transaction := Transacao; FSql := FQuery.SQL; end; destructor TQueryFD.Destroy; begin FQuery.Free; inherited; end; procedure TQueryFD.Executar; begin FQuery.Prepare; FQuery.ExecSQL; end; procedure TQueryFD.Abrir; begin FQuery.Open; end; function TQueryFD.Dataset: TDataset; begin Result := FQuery; end; function TQueryFD.RowsAffected: Integer; begin Result := FQuery.RowsAffected; end; function TQueryFD.RecordCount: Integer; begin Result := FQuery.RecordCount; end; function TQueryFD.Sql: TStrings; begin Result := FSql; end;
Template Method
Devemos reorganizar TDaoFireDac. A ideia aqui é criar uma classe base chamada TDaoBase. Esta nova classe conterá tudo o que não é específico de uma suíte de componente de acesso a dados. Ou seja, será uma classe abstrata contendo alguns métodos implementados. Um design pattern que se encaixa nesta definição é o Template Method. TDaoBase será o nosso template, e as classes filhas ( TDaoFireDac e TDaoIbx, etc. ) passarão herdar dela.
Abra Lca.Orm.Base, e declare essa nova classe:
... IDaoBaseComandos = interface //antigo IDaoBase ['{6E2AFB66-465B-4924-9221-88E283E81EA7}'] function GerarClasse(ATabela, ANomeUnit: string; ANomeClasse: string = ''): string; function GetID(ATabela:TTabela; ACampo: string): Integer; overload; function GetID(Generator: string): Integer; overload; function Inserir(ATabela: TTabela): Integer; overload; function Inserir(ATabela: TTabela; ACampos: array of string; ... TDaoBase = class(TInterfacedObject) private protected FSql: IBaseSql; FDataSet: TDataSet; FParams: IQueryParams; procedure SetDataSet(const Value: TDataSet); procedure BuscarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); virtual; abstract; procedure AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); virtual; abstract; procedure SetarDadosFromDataSet(ADataset: TDataset; PropRtti: TRttiProperty; Objeto: TValue; Campo: string); public constructor Create; property DataSet: TDataSet read FDataSet write SetDataSet; end;
Note que TDaoBase descende de TInterfacedObject. O motivo você entenderá quando da declaração da classe filha TDaoFiredac. Essa classe possui dois métodos abstratos, AtualizarRelacionamento e BuscarRelacionamento. Sendo abstrados, não são implementados diretamente nela, mas sim nas classes filhas. Segue implementação dos demais métodos de TDaoBase:
... { TDaoBase } constructor TDaoBase.Create; begin FSql := TPadraoSql.Create; end; procedure TDaoBase.SetarDadosFromDataSet(ADataset: TDataset; PropRtti: TRttiProperty; Objeto: TValue; Campo: string); var DataType: TFieldType; begin DataType := ADataSet.FieldByName(Campo).DataType; case DataType of ftInteger: begin PropRtti.SetValue(Objeto.AsObject, TValue.FromVariant(ADataSet.FieldByName(Campo).AsInteger)); end; ftString, ftWideString, ftWideMemo: begin PropRtti.SetValue(Objeto.AsObject, TValue.FromVariant(ADataSet.FieldByName(Campo).AsString)); end; ftBCD, ftFMTBcd, ftFloat: begin PropRtti.SetValue(Objeto.AsObject, TValue.FromVariant(ADataSet.FieldByName(Campo).AsFloat)); end; ftCurrency: begin PropRtti.SetValue(Objeto.AsObject, TValue.FromVariant(ADataSet.FieldByName(Campo).AsCurrency)); end; ftDate, ftDateTime: begin PropRtti.SetValue(Objeto.AsObject, TValue.FromVariant(ADataSet.FieldByName(Campo).AsDateTime)); end; else raise Exception.Create('Tipo de campo não conhecido: ' + PropRtti.PropertyType.ToString); end; end; procedure TDaoBase.SetDataSet(const Value: TDataSet); begin FDataSet := Value; end;
Em resumo movemos os objetos FSql e FDataSet da classe filha para a nova classe. Temos o método chamado SetarDadosFromDataSet, e como o próprio nome já diz, grava os dados vindos de um dataset e não de properties. Perceba que neste método os Datatypes são diferentes, pois vêm do TDataSet. Eis o motivo de precisarmos deste método.
Se observar atentamente, o mesmo poderia ter sido feito com a IQuery e TQueryFD. Porém, no atual estágio, nenhum dado poderia ser movido para a classe template. Então, no momento, não vejo vantagem de tal abordagem. Quem sabe futuramente.
Na declaração de TDaoFireDac, devemos fazer uma alteração, pois esta passará a implementar tanto TDaoBase quando IDaoBaseComandos. Agora você já sabe o motivo de TDaoBase herdar de TInterfacedObject. Segue código:
unit Lca.Orm.Comp.FireDac; interface ... TDaoFireDac = class(TDaoBase, IDaoBaseComandos) private FConexao: TFDConnection; FTransacao: TFDTransaction; protected ...
Finalmente, implementando o relacionamento
Ajustes feitos, vamos ao que realmente é objeto de interesse deste post.
O primeiro passo será criar um atributo que guarde os dados do relacionamento: chave estrangeira (FK), tabela relacionada, chave primária (Pk) da tabela relacionada e o tipo de dado.
Vá em Lca.Orm.Atributos, e insira o atributo chamado AttFk:
unit Lca.Orm.Atributos; interface uses Lca.Orm.Base, Rtti, System.Classes, Data.DB; ... AttFk = class(TCustomAttribute) private FCampoFk, FTabela, FPk: string; FTipo: TTypeKind; public constructor Create(CampoFk, Tabela, Pk: string; Tipo: TTypeKind = tkInteger); property CampoFk: string read FCampoFk; property Tabela: string read FTabela; property Pk: string read FPk; end; ...
Na implementação do constructor, apenas setamos os parâmetros recebidos nos fields correspondentes:
... constructor AttFk.Create(CampoFk, Tabela, Pk: string; Tipo: TTypeKind); begin FCampoFk := CampoFk; FTabela := Tabela; FPk := Pk; FTipo := Tipo; end; ...
Sem segredo até aqui. Apesar de simples, isso nos dará tudo o que é preciso para atingirmos nosso objetivo.
O próximo passo será ir nos métodos Inserir, Salvar, Buscar e ConsultaGen, e inserir o código responsável por processar o relacionamento. Iniciemos pelo método Inserir:
function TDaoFireDac.Inserir(ATabela: TTabela; ACampos: array of string; AFlag: TFlagCampos): Integer; var PropRtti: TRttiProperty; RttiType: TRttiType; AtribFk: AttFk; NomeTabela: string; Comando: IQuery; begin try TAtributos.Get.ValidaTabela(ATabela, ACampos, AFlag); RttiType := TRttiContext.Create.GetType(ATabela.ClassType); NomeTabela := TAtributos.Get.PegaNomeTab(ATabela); Comando := TQueryFD.Create(FConexao, FTransacao); Comando.Sql.Text := FSql.GerarSqlInsert(NomeTabela, RttiType, ACampos, AFlag); for PropRtti in RttiType.GetProperties do begin if (Length(ACampos) > 0) then begin if not (TAtributos.Get.LocalizaCampo(PropRtti.Name, TAtributos.Get.PegaPks(ATabela))) then begin if ((AFlag = fcIgnore) and (TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) or ((AFlag = fcAdd) and (not TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) then Continue; end; end; AtribFk := TAtributos.Get.GetAtribFk(PropRtti); if Assigned(AtribFk) then AtualizarRelacionamento(ATabela, PropRtti, AtribFk, Comando.Dataset) else TAtributos.Get.ConfiguraParametro(PropRtti, PropRtti.Name, ATabela, Comando.Dataset, FParams); end; Comando.Executar; Result := Comando.RowsAffected; except raise; end; end;
No código:
- Criamos um objeto do tipo do atributo AttFk se no loop atual existe um atributo do tipo de relacionamento (26);
- Se sim, executamos o método AtualizarRelacionamento (28). Este método foi definido na classe base como abstrato e ainda não foi implementado em TDaoFireDac;
- Se não, processamos normalmente a property, como era feito antes (30);
Como mencionado acima, o método AtualizarRelacionamento ainda não consta em TDaoFireDac. Portanto, vamos resolver esta pendência. Vá na seção protected da classe e insira:
... TDaoFireDac = class(TDaoBase, IDaoBaseComandos) private FConexao: TFDConnection; FTransacao: TFDTransaction; protected procedure AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); override; ...
Na implementação de AtualizarRelacionamento, temos:
... procedure TDaoFireDac.AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); var Objeto: TTabela; RttiType: TRttiType; Prop: TRttiProperty; begin if (APropRtti.GetValue(ATabela).AsObject is TTabela) then begin Objeto := (APropRtti.GetValue(ATabela).AsObject as TTabela); RttiType := TRttiContext.Create.GetType(Objeto.ClassType); for Prop in RttiType.GetProperties do begin if CompareText(Prop.Name, AttFK(Relacionamento).Pk) = 0 then begin TAtributos.Get.ConfiguraParametro(Prop, AttFK(Relacionamento).CampoFk, Objeto, AQuery, FParams); Break; end; end; if GetRecordCount(Objeto, [AttFk(Relacionamento).Pk]) = 0 then Inserir(Objeto) else Salvar(Objeto); end; end;
Segue explicação:
- Verificamos se a propriedade é do tipo TTabela (9). Essa propriedade contém os dados da tabela relacionada;
- Se sim, setamos o conteúdo da propriedade na variável Objeto (11);
- Fazemos então um loop em suas propriedades em busca de sua chave primária (15);
- Se é chave primária, seta o valor da propriedade no Objeto (17);
- Finalizado o loop, de posse do valor (ou valores) da chave primária, definimos se insere ou se salva os dados na tabela relacionada (21).
Para o método Salvar, segue conforme foi feito no Inserir:
function TDaoFireDac.Salvar(ATabela: TTabela; ACampos: array of string; AFlag: TFlagCampos): Integer; var PropRtti: TRttiProperty; RttiType: TRttiType; Comando: IQuery; AtribFk: AttFk; begin try TAtributos.Get.ValidaTabela(ATabela, ACampos, AFlag); RttiType := TRttiContext.Create.GetType(ATabela.ClassType); Comando := TQueryFD.Create(FConexao, FTransacao); Comando.Sql.Text := FSql.GerarSqlUpdate(ATabela, RttiType, ACampos, AFlag); for PropRtti in RttiType.GetProperties do begin if (Length(ACampos) > 0) and not (TAtributos.Get.LocalizaCampo(PropRtti.Name, TAtributos.Get.PegaPks(ATabela))) then begin if ((AFlag = fcAdd) and (not TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) or ((AFlag = fcIgnore) and (TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) then Continue; end; AtribFk := TAtributos.Get.GetAtribFk(PropRtti); if Assigned(AtribFk) then AtualizarRelacionamento(ATabela, PropRtti, AtribFk, Comando.Dataset) else TAtributos.Get.ConfiguraParametro(PropRtti, PropRtti.Name, ATabela, Comando.Dataset, FParams); end; Comando.Executar; Result := Comando.RowsAffected; except raise; end; end;
Nada de novo. Vamos para o método Buscar:
function TDaoFireDac.Buscar(ATabela: TTabela): Integer; var Campo: string; PropRtti: TRttiProperty; RttiType: TRttiType; AtribFk: AttFk; Query: IQuery; begin RttiType := TRttiContext.Create.GetType(ATabela.ClassType); Query := TQueryFD.Create(FConexao, nil); Query.Sql.Text := FSql.GerarSqlSelect(ATabela); for Campo in TAtributos.Get.PegaPks(ATabela) do begin for PropRtti in RttiType.GetProperties do begin if CompareText(PropRtti.Name, Campo) = 0 then begin TAtributos.Get.ConfiguraParametro(PropRtti, Campo, ATabela, Query.Dataset, FParams); Break; end; end; end; Query.Abrir; Result := Query.RecordCount; ATabela.Limpar; if Result > 0 then begin for PropRtti in RttiType.GetProperties do begin AtribFk := TAtributos.Get.GetAtribFk(PropRtti); if Assigned(AtribFk) then BuscarRelacionamento(ATabela, PropRtti, AtribFk, TFDQuery(Query.Dataset)) else TAtributos.Get.SetarDadosTabela(PropRtti, PropRtti.Name, ATabela, Query.Dataset, FParams); end; end; end;
A diferença é que neste método chamamos BuscarRelacionamento e não AtualizarRelacionamento. É preciso implementar este novo método em TDaoFireDac:
... TDaoFireDac = class(TDaoBase, IDaoBaseComandos) private FConexao: TFDConnection; FTransacao: TFDTransaction; protected procedure AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); override; procedure BuscarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); override; ...
Implementação do método BuscarRelacionamento:
procedure TDaoFireDac.BuscarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty; Relacionamento: TCustomAttribute; AQuery: TDataSet); var Contexto: TRttiContext; Objeto: TTabela; RttiType: TRttiType; Prop: TRttiProperty; KeyLocalized: Boolean; begin KeyLocalized := False; if (APropRtti.GetValue(ATabela).AsObject is TTabela) then begin Contexto := TRttiContext.Create; try Objeto := (APropRtti.GetValue(ATabela).AsObject as TTabela); RttiType := Contexto.GetType(Objeto.ClassType); for Prop in RttiType.GetProperties do begin if CompareText(Prop.Name, AttFk(Relacionamento).Pk) = 0 then begin TAtributos.Get.SetarDadosTabela(Prop, AttFk(Relacionamento).CampoFk, Objeto, AQuery, FParams); KeyLocalized := True; Break; end; end; if KeyLocalized then Buscar(Objeto); finally Contexto.Free; end; end; end;
Bem simples, não é mesmo? Assim como no AtualizarRelacionamento, setamos a chave primário no Objeto (21). Se foi localizado a chave primária, buscamos o Objeto (27).
Por fim, o método ConsultaGen. E este eu lhes digo que tem uma pegadinha. Vamos a ele:
function TDaoFireDac.ConsultaGen<T>(ATabela: TTabela; ACamposWhere: array of string): TObjectList<T>; var Contexto: TRttiContext; Campo: string; TipoRtti: TRttiType; PropRtti: TRttiProperty; Query: IQuery; Objeto: TValue; AtribFk: AttFk; begin Contexto := TRttiContext.Create; try Result := TObjectList<T>.Create; TipoRtti := Contexto.GetType(ATabela.ClassType); Query := TQueryFD.Create(FConexao, nil); Query.SQL.Text := FSql.GerarSqlSelect(ATabela, ACamposWhere); for Campo in ACamposWhere do begin if not TAtributos.Get.PropExiste(Campo, PropRtti, TipoRtti) then raise Exception.Create('Campo ' + Campo + ' não existe no objeto!'); for PropRtti in TipoRtti.GetProperties do begin if CompareText(PropRtti.Name, Campo) = 0 then begin TAtributos.Get.ConfiguraParametro(PropRtti, Campo, ATabela, Query.Dataset, FParams); Break; end; end; end; Query.Abrir; while not Query.Dataset.Eof do begin Objeto := TObjectFactory<T>.Get.CriarInstancia; TipoRtti := Contexto.GetType(ATabela.ClassType); for PropRtti in TipoRtti.GetProperties do begin AtribFk := TAtributos.Get.GetAtribFk(PropRtti); if Assigned(AtribFk) then BuscarRelacionamento(Objeto.AsType<T>, PropRtti, AtribFk, Query.Dataset) else SetarDadosFromDataset(Query.Dataset, PropRtti, Objeto.AsType<T>, PropRtti.Name); end; Result.Add(Objeto.AsType<T>); Query.Dataset.Next; end; finally Contexto.Free; end; end;
Note que em vez de:
Objeto := (APropRtti.GetValue(ATabela).AsObject as TTabela);
Temos:
Objeto := TObjectFactory<T>.Get.CriarInstancia;
Ou seja, tivemos de utilizar uma fábrica de objeto (que ainda será criada, diga-se de passagem) em detrimento da primeira opção. O motivo é simples, pois veja que no primeiro, instanciamos o objeto como sendo do tipo TTabela. Tudo certo se não fosse por um pequeno detalhe: o Objeto terá todo o comportamento previsto em uma classe TTabela, inclusive seu construtor. E é aí que inviabiliza esta forma de utilização. Pois, vejamos a entidade Funcionario, não mencionada até o momento, mas em tempo o faço abaixo:
unit Funcionario; interface uses Lca.Orm.Atributos, Lca.Orm.Base, Cidade, Depto; type [AttTabela('FUNCIONARIO')] TFuncionario = class(TTabela) private FBairro: string; FSalario: Currency; FId: Integer; FDepto: TDepto; FNome: string; FCidade: TCidade; FEndereco: string; FCpf: string; public constructor Create; destructor Destroy; override; [AttPK] property Id: Integer read FId write FId; property Nome: string read FNome write FNome; property Cpf: string read FCpf write FCpf; property Endereco: string read FEndereco write FEndereco; property Bairro: string read FBairro write FBairro; [AttNotNull('Cidade')] [AttFk('IDCIDADE', 'CIDADE', 'ID')] property Cidade: TCidade read FCidade write FCidade; [AttNotNull('Departamento')] [AttFk('IDDEPTO', 'DEPTO', 'ID')] property Depto: TDepto read FDepto write FDepto; property Salario: Currency read FSalario write FSalario; end; implementation { TFuncionario } constructor TFuncionario.Create; begin FCidade := TCidade.Create; FDepto := TDepto.Create; end; destructor TFuncionario.Destroy; begin FCidade.Free; FDepto.Free; inherited; end;
Para esta classe não utilizamos o construtor padrão de um TObject, e sim um modificado para a instanciação dos objetos das tabelas relacionadas.
Entende como isso muda tudo? Se utilizarmos o construtor de TTabela, não estaremos instanciando os objetos relacionados a TFuncionario, e ao tentar acessar os atributos Cidade e Departamento desta classe obteremos um Access Violation na fuça!
Portanto, é necessário criar a fábrica de objetos. Para isso, abra Lca.Orm.Base e declare a interface e classe:
... IObjectFactory<T:TTabela> = interface ['{50ACF26D-52D9-490A-B22D-F672B344AB94}'] function CriarInstancia: T; end; TObjectFactory<T:TTabela> = class (TInterfacedObject, IObjectFactory<T>) public class function Get: IObjectFactory<T>; function CriarInstancia: T; end; ...
Segue implementação dos métodos:
... { TObjectFactory<T> } function TObjectFactory<T>.CriarInstancia: T; var AValue: TValue; Contexto: TRttiContext; TipoRtti: TRttiType; MetodoCriar: TRttiMethod; TipoInstancia: TRttiInstanceType; begin Contexto := TRttiContext.Create; try TipoRtti := Contexto.GetType(TypeInfo(T)); MetodoCriar := TipoRtti.GetMethod('Create'); if Assigned(MetodoCriar) and TipoRtti.IsInstance then begin TipoInstancia := TipoRtti.AsInstance; AValue := MetodoCriar.Invoke(TipoInstancia.MetaclassType, []); Result := AValue.AsType<T>; end; finally Contexto.Free; end; end; class function TObjectFactory<T>.Get: IObjectFactory<T>; begin Result := TObjectFactory<T>.Create; end; ...
O método CriarInstancia basicamente localiza o método Create do tipo T (genérico) e o invoca (executa). O resultado é retornado na Linha 20. Assim, garantimos que o constructor chamado será o do tipo passado (T) e não o constructor padrão de TTabela. Como, ao consultar, estaremos fazendo algo do tipo:
Lista := Dao.ConsultaGen<TFuncionario>(Funcionario, ...
Ou seja, passamos o tipo TFuncionario, o construtor chamado será de TFuncionario e não de TTabela. Espero que eu tenha sido claro. 😒
De código para o relacionamento, pelo menos para início dos testes, é isso.
Testes
Junto com os fontes, vai um novo form (ufrmTesteRelacionamento) para os testes:
Com relação ao código do botão de busca genérica, cabe um pequeno adendo:
var Lista: TObjectList<TFuncionario>; I: Integer; begin Memo.Lines.Clear; Memo.Lines.Add('Teste do método ConsultaGen, obtendo como retorno objeto(s) do tipo especificado.'); Memo.Lines.Add(''); Funcionario.Limpar; Funcionario.Id := StrToIntDef(edCodFunc.Text, 0); Lista := Dao.ConsultaGen<TFuncionario>(Funcionario, ['Id']); try for I := 0 to Lista.Count - 1 do begin Funcionario.CopyProps(Lista.Items[I]); Memo.Lines.Add('Registro no DataSet: ' + IntToStr(Funcionario.Id)); Memo.Lines.Add(''); AtualizaForm; end; finally Lista.Free; end; end;
Note que na linha 14, foi adicionado um novo método para agilizar a cópia dos dados de um objeto para outro. O método CopyProps varre a propriedade de objeto passado no parâmetro e repassa os valores para objeto que chamou o método. Segue o código (unit Lca.Orm.Base, classe TTabela):
... TTabela = class public procedure Limpar; procedure CopyProps(From: TTabela); end; ... implementation ... procedure TTabela.CopyProps(From: TTabela); var Contexto: TRttiContext; TipoRtti, TipoFrom: TRttiType; PropRtti, PropFrom: TRttiProperty; begin Contexto := TRttiContext.Create; try TipoRtti := Contexto.GetType(Self.ClassType); TipoFrom := Contexto.GetType(From.ClassType); for PropRtti in TipoRtti.GetProperties do begin for PropFrom in TipoFrom.GetProperties do if SameText(PropFrom.Name, PropRtti.Name) then begin if PropRtti.PropertyType.TypeKind = tkClass then begin (PropRtti.GetValue(Self).AsObject as TTabela).CopyProps((PropFrom.GetValue(From).AsObject as TTabela)); end else PropRtti.SetValue(Self, PropFrom.GetValue(From)); Break; end; end; finally Contexto.Free; end; end;
Nada de novo, temos apenas um loop que varre as propriedades e repassa para o objeto que chamou o método. Porém, na linha 27, verificamos se o tipo de dado é tkClass, ou seja, verificamos se é uma classe relacionada a uma tabela, se sim, recursivamente, chamamos o método CopyProps, e assim atualizamos a tabela relacionada também.
No código do botão de inserir:
var Registros: Integer; begin Memo.Clear; Memo.Lines.Add('Teste do método Inserir.'); Memo.Lines.Add(''); FormToObjetos; Dao.StartTransaction; try Funcionario.Id := Dao.GetID(Funcionario, 'Id'); Registros := Dao.Inserir(Funcionario); Dao.Commit; AtualizaForm; Memo.Lines.Add(Format('Registro inserido: %d', [Registros])); except on E: Exception do begin Dao.RollBack; ShowMessage('Ocorreu um problema ao executar operação: ' + e.Message); end; end; end;
Nele, chamamos o método FormToObjetos, que simplesmente irá pegar os valores inseridos nos edits do form e repassar para objeto Funcionario. O grande destaque é que, na linha 11, apenas chamando o método inserir para um objeto, que no caso é o Funcionario, já irá atualizar também as tabelas Depto e Cidade. Segue código de FormToObjetos:
procedure TfrmTesteRelacionamento.FormToObjetos; var Fmt: TFormatSettings; begin Funcionario.Id := StrToIntDef(edCodFunc.Text, 0); Funcionario.Nome := edNomeFunc.Text; Funcionario.Cpf := edCpf.Text; Funcionario.Endereco := edEndereco.Text; Funcionario.Bairro := edBairro.Text; Funcionario.Cidade.Id := StrToIntDef(edCodCidade.Text, 0); Funcionario.Cidade.Nome := edNomeCidade.Text; Funcionario.Cidade.Uf := edUF.Text; Funcionario.Cidade.Ibge := StrToIntDef(edIbge.Text, 0); Funcionario.Depto.Id := StrToIntDef(edCodDep.Text, 0); Funcionario.Depto.Nome := edNomeDep.Text; Fmt := TFormatSettings.Create; Fmt.DecimalSeparator := ','; Funcionario.Salario := StrToFloat(StringReplace(edSalario.Text, '.', '', [rfReplaceAll]), Fmt); end;
Estes são os destaques que eu achei importante pontuar na hora dos testes.
Considerações finais
Este post já está bem extenso, então irei parar por aqui. No Github ( https://github.com/luizsistemas/ORM-Basico-Delphi ), você encontra os fontes atualizados.
Com relação ao relacionamento, já conseguimos consultar um objeto e este já vem com os objetos relacionados a ele. Da mesma forma, ao inserir e salvar. Para melhor desempenho, dica de uma possível alteração, seria a de implementar o design pattern Lazy Loading, e somente carregar os dados das tabelas relacionadas quando este for acessado. Ou então, na alteração de dados, ter uma flag para informar se deseja ou não atualizar determinada tabela. São, digamos, ajustes bem vindos. Mas por ora, fico por aqui.
Abraços.
Olá Luiz, Gostaria de ver com você um tema que pouco amplo na minha opinião sobre o design patterns MVP (Model-View-Presenter) para sistemas em pascal Delphi.
Gosto muito do seu projeto de ORM para facilitar o uso do MCV mas gostaria de ir mais além com a utilização do MVP sendo aplicada na prática.
Quando puder comentar e talvez criar uma postagem sobre o assunto fica a minha sugestão!
Anderson,
Já tem um post na DevMedia bastante interessante sobre o tema: https://www.devmedia.com.br/o-padrao-mvp-model-view-presenter/3043
Eu particularmente, por preferência, utilizo majoritariamente em meus projetos o MVC.