Mapeamento Objeto-Relacional com TMS Aurelius
Veremos neste artigo como funciona e como utilizar o framework de Mapeamento Objeto-Relacional TMS Aurelius, que permite lidar com bancos de dados aproveitando os recursos da orientação a objetos.
Historicamente, o Delphi sempre foi reconhecido por sua excelência RAD, com componentes visuais prontos para uso, no mais tradicional arrastar-e-soltar. Aliado a isso também é reconhecido pela facilidade com que provê a construção de aplicações de banco de dados. Neste cenário, o uso de DataSets e controles data-aware (os famosos controles “DB” - DBEdit, DBGrid e cia) se combina a uma programação estruturada bastante eficiente por meio de procedures e functions. Até este ponto, todos os recursos necessários são basicamente nativos, providos pela própria ferramenta, numa instalação comum. Todavia, no cotidiano, este contexto pode seguir por outros direcionamentos, tal qual é o caso da POO (Programação Orientada a Objetos).
A Programação Orientada a Objetos acaba por definir outro conceito de desenvolvimento, agora baseado em objetos relacionados ao mundo real. Passando isso para o contexto das aplicações de banco de dados faz com que não se tenha mais simples DataSets manipulando instruções SQL a serem enviadas ao banco de dados, mas sim a manipulação de efetivos objetos de negócio. Tendo em vista esta abordagem, a estrutura provida pela aplicação, agora baseada em objetos, acaba por se tornar incompatível com a própria estrutura relacional do banco de dados. Visando superar este tipo de barreira é que surgem os frameworks ORM (Object-Relational Mapping, ou Mapeamento Objeto-Relacional, em português).
Em termos práticos, o que um framework ORM faz é interpretar os dados envolvidos, transformando então objetos de negócio em dados relacionais, bem como o caminho inverso, onde as informações provindas de uma base de dados dão origem a estes objetos. Durante esta jornada, as instruções SQL envolvidas acabam sendo suprimidas de qualquer contato direto com o desenvolvedor, dando origem a métodos concretos que, no final das contas, simplificam todo o processo de codificação.
Dito isto, este artigo transpõe então todo este cenário ao contexto do Delphi, por meio da apresentação de uma solução bastante eficaz e condizente com toda a praticidade e facilidade já conhecidos do IDE.
TMS Aurelius
O TMS Aurelius, ou simplesmente Aurelius, é um framework ORM exclusivo para Delphi, produzido pela empresa TMS Software. Sua distribuição se dá de forma comercial, ou seja, há a necessidade da aquisição de uma licença para o seu uso em produção, porém o framework desfruta de uma série de fatores e recursos que justificam a sua compra e plena utilização.
Licença
Em suma, o Aurelius é oferecido em três opções, que se diferenciam basicamente pela quantidade de usuários em uso:
- Single Developer License: habilita a utilização para um único usuário (desenvolvedor);
- Small Team: habilita a utilização para uma equipe pequena de até dois desenvolvedores;
- Site License: habilita a utilização para um número irrestrito de usuários na empresa relacionada;
Para fins de testes e conhecimento, o framework dispõe de um período de avaliação (Trial), suficiente para comprovar toda a sua eficiência no que ele se propõe a fazer.
Versões suportadas do Delphi
Uma tendência bastante positiva do Aurelius é o seu suporte atualizado às mais recentes versões do Delphi, o que significa dizer que muito provavelmente você terá uma nova versão disponível sempre que um novo release do IDE for lançado. De forma oficial, o suporte do framework se inicia no Delphi 2010, passando pela família XE (XE, XE2, XE3, etc.), Delphi 10 Seattle, até o recente Delphi 10.1 Berlin (até o momento da escrita deste artigo).
Componentes
Um cenário bastante comum no contexto Delphi é a aquisição de uma biblioteca de componentes que, uma vez instalada, disponibiliza na Tool Palette do IDE uma série de elementos prontos para uso pelo simples arrastar-e-soltar em um Form ou Data Module. Com o TMS Aurelius isso foge um pouco do tradicional, uma vez que sua instalação provê apenas um único elemento na paleta de componentes do Delphi (Figura 1). Isto porque, toda sua essência se dá por meio de sua estrutura de classes e interfaces provenientes de seu framework. Assim como veremos mais adiante, o uso do Aurelius em uma aplicação Delphi se dá essencialmente por meio de código, o que torna o processo bastante intuitivo com o passar do tempo.
Por que utilizar o TMS Aurelius?
As razões para se utilizar o Aurelius podem ser inúmeras e variáveis, tudo irá depender do contexto ao qual estará inserido. Falando de forma mais abrangente, o framework em si se baseia em três pilares que estabelecem os principais benefícios que uma aplicação irá obter com o seu uso: produtividade, manutenibilidade e portabilidade.
Ganho em produtividade
O termo produtividade diz respeito à forma simples com que o Aurelius lida com a manipulação de dados. Neste aspecto, toda a codificação da aplicação será voltada diretamente a objetos, e não mais a instruções SQL. De forma ilustrativa, na Listagem 1 é exibido um trecho de código tradicional, utilizando FireDAC para a seleção de um determinado registro da tabela CUSTOMER do banco Employee do Firebird. Na mesma medida, na sequência é exibido como ficaria esta mesma instrução utilizando-se o Aurelius.
// forma tradicional FDQuery1.SQL.Clear;
FDQuery1.SQL.Add('select CUST_NO, CUSTOMER, PHONE_NO, CITY, STATE_PROVINCE,
COUNTRY from CUSTOMER where CUST_NO = :CUST_NO');
FDQuery1.ParamByName('CUST_NO').AsInteger := 1001;
FDQuery1.Open;
// usando TMS Aurelius
Customer := Manager1.Find<TCustomer>(1001);
Além de um código mais enxuto, outra vantagem disso é que por se tratar de objetos Delphi, qualquer erro relacionado à sintaxe será verificado ainda em design-time pelo compilador, ao contrário de uma instrução SQL na forma de string, que só será verificada em runtime.
Manutenção de código
O Aurelius acaba por abstrair todo o conhecimento sobre a camada de acesso a dados e instruções SQL envolvidas na aplicação. Assim, o desenvolvedor volta seu foco apenas ao uso da estrutura provida pelo framework, o que tende a simplificar diversos cenários, facilitando em termos de manutenção. Prova disso é o próprio exemplo anterior que mostra como uma codificação baseada no Aurelius torna-se muito mais enxuta. Outra menção que pode ser feita aqui é que, independente do banco de dados a ser atendido (se Firebird, Oracle, MySQL ou outro), uma mesma codificação será utilizada, ao contrário dum cenário mais tradicional com DataSets, onde teríamos instruções SQL específicas a cada SGBD, por exemplo.
Código portável
Aqui, entenda como portabilidade a capacidade do Aurelius de atender a variados bancos de dados a partir de uma mesma base de código. Num aspecto prático, o desenvolvedor estará lidando essencialmente com uma estrutura única de objetos, deixando a cargo do framework as tratativas inerentes ao acesso e manipulação efetiva dos dados.
Bancos de dados suportados
O TMS Aurelius oferece suporte a uma enorme gama de banco de dados, o que acaba por englobar as principais opções do mercado atual. Ao total são onze:
- Firebird;
- Interbase;
- Oracle;
- MS SQL Server;
- MySQL;
- PostgreSQL;
- SQLite;
- DB2;
- ElevateDB Server;
- Absolute Database;
- Nexus DB.
Componentes de acesso a dados suportados
Conforme será visto mais adiante, o TMS Aurelius faz uso de componentes já tradicionais de acesso a dados para toda a parte de conectividade de seu framework. Sendo assim, seu suporte oficial contempla as principais opções nativas e de terceiro, já bastante populares em meio à comunidade, tais como:
- dbExpress;
- ADO (dbGo);
- FireDac;
- IBX (Interbase Express);
- AnyDac;
- IBObjects;
- UniDac;
- Zeos;
- FIBPlus,
- UIB (Unified Interbase).
Plataformas suportadas
Além disso, o TMS Aurelius também se caracteriza por ser uma solução multiplataforma, podendo ser usado tanto em projetos VCL quanto em projetos FireMonkey (FMX), atendendo assim todas as plataformas suportadas pelo Delphi atualmente: Windows (32-bit e 64-bit), MacOS, iOS e Android.
Conectividade a banco de dados
As nuances do TMS Aurelius são inúmeras, o que leva cada ponto do framework a contemplar uma enorme gama de conceitos e diretrizes específicas. Obviamente este artigo primará pelos principais tópicos, tomando como base inicial o seu aspecto em termos de conectividade com banco de dados.
IDBConnection e Adapters
Numa aplicação que faz uso do Aurelius, o objeto que representa uma conexão com o banco de dados é do tipo IDBConnection. Conforme sua própria nomenclatura sugere, IDBConnection nada mais é do que uma Interface, declarada na unit Aurelius.Drivers.Interfaces do framework. De forma prática, para qualquer objeto da aplicação que necessite se conectar ao banco para enviar ou receber dados, deverá então necessariamente utilizar esta interface. Em seus bastidores, o que o IDBConnection faz é encapsular um componente de acesso a dados existente, tomando pra si a conectividade pré-estabelecida, criando uma camada de uso transparente ao usuário. Assim, independente do SGBD ou tecnologia de acesso a dados, a referência sempre será feita a um objeto IDBConnection.
Por fim, a obtenção de um IDBConnection se dá pelo uso de adaptadores (Adapters) que a implementam e que são providos pelo próprio framework. A cada componente de acesso a dados suportado pelo Aurelius há um adaptador relacionado, declarado em uma unit específica. Tomando como exemplo as duas bibliotecas nativas de acesso a dados mais populares, dbExpress e FireDac, temos:
dbExpress
- Componente de acesso a dados: TSQLConnection
- Classe adaptadora: TDBExpressConnectionAdapter
- Unit: Aurelius.Drivers.dbExpress
FireDac
- Componente de acesso a dados: TFDConnection
- Classe adaptadora: TFireDacConnectionAdapter
- Unit: Aurelius.Drivers.FireDac
A lista completa pode ser obtida na própria documentação do produto, disponível em seu site oficial (ver seção Links).
Conectando a uma base de dados
Uma vez de posse da base teórica do processo de conectividade do framework, sua transposição para o aspecto prático se torna bastante facilitado. Na Listagem 2 é mostrado então um trecho de código de exemplo necessário para a obtenção de uma conexão válida.
var
MinhaConexao: IDBConnection;
begin
MinhaConexao := TFireDacConnectionAdapter.Create(FDConnection1, False);
…
end;
De acordo com o trecho mostrado, a obtenção de um objeto IDBConnection se dá pelo uso de um adaptador para FireDac (TFireDacConnectionAdapter). Em termos gerais, o construtor dos diversos Adapters assume assinaturas padrões provindas de sobrecargas de seu método Create. Para o exemplo mostrado, a assinatura possui a seguinte definição:
constructor Create(AConnection: T; AOwnsConnection: boolean);
AConnection refere-se ao componente de acesso a dados a ser utilizado, neste caso, um FDConnection (FDConnection1). Já AOwnsConnection indica se o componente indicado será destruído ou não juntamente do IDBConnection. Uma vez definido como False, o componente permanecerá em memória. Isso dá margens para, por exemplo, fazer uso do Aurelius em uma aplicação já existente, mantendo-se em paralelo uma abordagem tradicional com DataSets e outra com uso de objetos apoiada pelo framework.
TMS Aurelius Connection
TMS Aurelius Connection faz referência a um Connection Wizard (assistente de conexão) provido pela instalação do framework, e que fica disponível na seção de projetos do IDE (File > New > Other) conforme mostra a Figura 2. Sua função é apoiar o desenvolvedor na definição da conexão a dados de uma aplicação Aurelius, solicitando para isso apenas duas informações (Figura 3): Driver e SQL Dialect.
Driver faz referência ao componente de acesso a dados a ser utilizado enquanto que SQL Dialect diz respeito ao dialeto a ser considerado durante a execução de instruções SQL. Isto porque, é sabido que cada SGBD possui suas próprias nuances de SQL, logo, esse tipo de informação é vital para que o framework possa gerar as instruções SQL adequadas. Internamente no Aurelius cada SQL Dialect é identificado por uma string, condizente ao banco relacionado, tal como ‘Firebird’, ‘Interbase’, ‘MySQL’ e assim por diante. Para Adapters de tecnologias multi-banco, como é o caso de FireDac e dbExpress, o framework é capaz de obter por si só o dialeto apropriado, por meio do nome do driver associado ao componente de conexão. Por outro lado, há casos onde esta identificação não é possibilitada, tornando obrigatório o provimento da indicação explícita do dialeto. Em vista disso, o assistente prevê o fornecimento do SQL Dilect desejado, atendendo assim de forma segura ambos os cenários.
Após a finalização do Wizard é então criado um novo DataModule contendo o componente de acesso indicado, conforme mostra a Figura 4. Adicionalmente, o assistente se encarrega também de gerar uma porção de código na unit do DataModule, que envolve, entre outras coisas, a declaração do método que ficará responsável por retornar um IDBConnection válido, a ser utilizado por todo o projeto. A Listagem 3 exibe o corpo deste método, tendo como base o cenário demonstrativo inicial com FireDac e Firebird.
class function TFireDacFirebirdConnection.CreateConnection: IDBConnection;
var
DataModule: TFireDacFirebirdConnection;
begin
DataModule := TFireDacFirebirdConnection.Create(nil);
Result := TFireDacConnectionAdapter.Create(DataModule.Connection,
'Firebird', DataModule);
end;
CreateConnection é então o nome do método de classe criado pelo assistente e que se fundamenta pela instanciação do DataModule (TFireDacFirebirdConnection) e retorno da interface IDBConnection. No Delphi, um class method nada mais é do que uma procedure ou function que opera sobre uma referência de classe e não de objeto. Por fim, na linha 6 dessa listagem é possível ver a chamada ao construtor da classe Adapter envolvida, neste caso TFireDacConnectionAdapter, que irá retornar a interface desejada. Conforme mencionado anteriormente, no framework, este tipo de construtor apresenta várias sobrecargas. Assim, aqui é utilizada a seguinte assinatura, em que ASQLDialect refere-se ao dialeto a ser utilizado (neste caso, ‘Firebird’) e OwnedComponent indica o componente que será destruído juntamente da interface (neste caso, o próprio Data Module):
constructor Create(AConnection: T; ASQLDialect: string; OwnedComponent:
TComponent);
Mapeamento de classes
No contexto Orientado a Objeto ao qual o TMS Aurelius se encaixa, o início dos trabalhos efetivos com o framework realmente tem início com a definição e mapeamento das classes de negócio da aplicação. Seguindo um aspecto mais purista, é neste ponto em que serão definidas, por exemplo, as classes de negócio, e não mais uma visão linear pela simples criação das tabelas do banco de dados.
Com o uso do Aurelius, tais classes nada mais são do que classes Delphi simples, mapeadas ao propósito do framework. Este mapeamento é a indicação dos detalhes da equivalência de cada ponto de sua estrutura, que servirá para que o Aurelius tome conhecimento sobre como esta deverá ser persistida no banco de dados.
Automapping
Automapping é um recurso do Aurelius que, conforme seu próprio nome sugere, automatiza todo o processo de mapeamento de classes, facilitando em muito a vida do desenvolvedor. Obviamente, assim como todo processo automatizado, seu uso requer certos cuidados, sendo indicado para cenários mais iniciais. Para um melhor entendimento, nada melhor que uma demonstração baseada em código, assim, a Listagem 4 exibe uma simples classe Delphi, denominada TRevista, já decorada com o atributo Automapping.
[Automapping]
[Entity]
TRevista = class
private
FId: Integer;
FAssunto: string;
FTituloCapa: string;
FEdicao: Integer;
public
property Id: Integer read FId write FId;
property Assunto: string read FAssunto write FAssunto;
property TituloCapa: string read FTituloCapa write FTituloCapa;
property Edicao: Integer read FEdicao write FEdicao;
end;
Em termos de código, esta classe é toda declarada utilizando código Delphi nativo, com exceção dos atributos Automapping, já citado e Entity. Este último indica ao Aurelius que esta classe é uma entidade que poderá ser persistida no banco de dados. Além disso, para que os atributos do framework possam ser reconhecidos e devidamente interpretados, é necessária a declaração da seguinte unit na seção uses da codificação:
Aurelius.Mapping.Attributes
Assim, apenas por esta indicação de Automapping, o Aurelius consegue fazer as devidas associações para que esta classe seja espelhada em uma tabela do banco de dados. Como exemplo, a entidade mostrada daria origem então a uma tabela nomeada como Revista, contendo quatro colunas (ID, ASSUNTO, TITULO_CAPA e EDICAO) e uma chave primária (ID). Toda essa estrutura resultando pode ser visto na Figura 5.
Mapeamento manual
Conforme citado, o Automapping é indicado para cenários iniciais, uma vez que proporciona baixo controle de suas atribuições. Como exemplo imediato é possível citar a estrutura criada anteriormente, onde as colunas da tabela Revista foram definidas de acordo com um padrão estabelecido pelo próprio Aurelius. Assim, os campos string foram mapeados para campos VARCHAR de tamanho máximo (255) e obrigatório (Not Null).
Isso significa dizer que num cenário real, em que há a necessidade por um controle total da estrutura de entidades que está sendo definida, é inevitável o uso de um mapeamento manual. Para estes casos, o framework conta com um leque bastante vasto de atributos que irão “decorar” uma classe, cada qual com um objeto específico. Para melhor entendimento dos conceitos envolvidos, nada melhor do que traçar um paralelo prático de uso. Sendo assim, a Listagem 5 exibe a definição do mesmo modelo de classe utilizado anteriormente, agora com um mapeamento customizado, seguindo as diretrizes do Aurelius. Na sequência são expostos os conceitos envolvidos a cada atributo utilizado. Vale ressaltar que esta estrutura de classe servirá então como base por todo o artigo.
[Entity]
[Id('FId', TIdGenerator.IdentityOrSequence)]
[Sequence('SEQ_REVISTA')]
[Table('REVISTA')]
TRevista = class
private
[Column('REVISTA_ID', [TColumnProp.Required])]
FId: Integer;
[Column('ASSUNTO', [TColumnProp.Required], 80)]
FAssunto: string;
[Column('TITULO_CAPA', [], 80)]
FTituloCapa: Nullable<string>;
[Column('EDICAO', [TColumnProp.Required])]
FEdicao: Integer;
public
property Id: Integer read FId write FId;
property Assunto: string read FAssunto write FAssunto;
property TituloCapa: Nullable<string> read FTituloCapa
write FTituloCapa;
property Edicao: Integer read FEdicao write FEdicao;
end;
Sem deixar de citar, a Figura 6 ilustra o resultado obtido pelo mapeamento manual da classe REVISTA.
Entity
Entity é um atributo básico, já citado anteriormente, que diz ao framework que a classe especificada é uma Entidade. No Aurelius, toda entidade pode ser persistida no banco de dados.
Id
“Id” Indica o elemento da classe que será usado como seu identificador único, tal qual o conceito de Chave Primária em uma tabela do banco de dados. No contexto de uma Classe, este elemento poderá ser então um campo (Field) ou propriedade (Property). Esse tipo de atribuição é uma determinação do próprio framework que exige que todo objeto possua uma identificação única, para que possa ser devidamente manipulado.
Adicionalmente, como parâmetro, é passado um valor prefixado com TIdGenerator, que indica a forma com que o valor será gerado para o identificador. IdentityOrSequence determina então que tal valor será gerado pelo banco de dados, seja por meio de campo auto incremento, ou objetos Generator e Sequence, bastante tradicionais em bases InterBase e Firebird. Nesta situação, o framework apenas espera pelo valor gerado. Além de IdentityOrSequence, outras opções são disponibilizadas, tal qual None, que deixa a cargo da aplicação a geração do valor do identificador.
Sequence
Para os casos em que há o envolvimento de um objeto Sequence ou Generator de banco de dados, tal como é o do exemplo proposto, o atributo “Sequence” deverá ser utilizado para indicá-los de forma explícita ao framework. Sendo assim, na maioria dos casos, seu uso fica atrelado à própria definição do Id.
Table
O atributo Table pode ser tido como essencial, uma vez que faz o mapeamento da classe a uma tabela do banco de dados. Entre outras coisas, isso determina que todo objeto oriundo desta classe será salvo como um registro desta tabela. Assim, tomando como base a classe Revista de exemplo, seu mapeamento se dá a uma tabela Revista no banco de dados. Na aplicação, conforme será visto adiante, uma vez que uma instância de Revista é salva no contexto do Aurelius, seus dados são persistidos em forma de registro na tabela citada.
Column
A função do atributo Column é bastante intuitiva uma vez que mapeia o campo da Classe à coluna na tabela do banco de dados. Como argumentos, recebe a indicação de campo requerido (TColumnProp.Required) e seu tamanho, para os casos de campos String.
Nullable types
Em frameworks ORM o termo “Nullable types” refere-se a tipos que podem receber a atribuição de nulidade (null). Visto isto no lado prático, é o que ocorre com grande parte dos campos que não possuem preenchimento obrigatório, ou seja, que podem ter um valor associado ou permanecem sem valor algum (null). Numa situação natural com o TMS Aurelius, pressupõe-se então que a simples definição de um campo na classe e seu mapeamento a uma coluna de uma tabela no banco de dados já seria suficiente para se trabalhar com valores nulos. No entanto, os tipos primitivos do Delphi não suportam valores nulos, o que acarreta erros em runtime (Figura 7). Em vista disso, o Aurelius apresenta o tipo Nullable<T>, declarado na unit Aurelius.Types.Nullable, que já faz o tratamento automático para os casos de nulidade ao campo.
Tomando como base a codificação de exemplo, o campo FCapa é então declarado como sendo do tipo Nullable<string>. Logo, caso este não receba valor algum, ele será devidamente persistido como null no banco.
Blobs
Indo além do exemplo, outro tipo bastante comum ao desenvolvimento de aplicações de banco de dados é o tipo Blob. Tradicionalmente ele é utilizado para armazenar dados binários (ex: imagens, documentos, planilhas) em uma dada coluna da tabela do banco. No Aurelius, campos Blob não recebem uma marcação especial, mas sim uma tipagem de dados especificada. Por recomendação, deve-se utilizar o tipo TArray<byte> ou TBlob, que fazem referência a uma matriz de bytes. Este último é o mais recomendado por questões de usabilidade do próprio framework.
Essa usabilidade citada se refere a outro recurso atrelado ao uso deste tipo, denominado Lazy-Loading Blobs. Numa tradução livre para o português, o termo lazy-loading equivale a um “carregamento preguiçoso”, o que resume bem sua funcionalidade. Como se sabe, uma informação binária pode possuir uma grande quantidade de bytes associados, tal como uma imagem grande. Logo, o carregamento de campos Blobs pode acabar por degradar bastante a aplicação, em termos de performance, comprometendo assim a usabilidade do usuário.
Pensando nisso, o TMS Aurelius estabelece que os campos Blobs marcados como Lazy-Loading não sejam recuperados num primeiro momento, e sim somente quando forem requisitados. Na prática, sua definição ocorre da seguinte forma:
[Column(‘IMAGEM_CAPA’, [TColumnProp.Lazy]) FImagemDeCapa: TBlob;
O legado: chave primária composta
Conforme pôde ser observado, o conceito trazido pelo TMS Aurelius incentiva o uso de identificadores únicos a cada objeto, o que mantém sua devida singularidade, bem como simplifica todo o aspecto de negócio e conhecimento envolvido. Na prática, a tendência então se estabelece pela criação de classes contendo um elemento Id, que é o seu identificador tanto no contexto da aplicação, quanto no contexto do banco de dados (chave primária da tabela). Todavia, o propósito do Aurelius não é somente atender a construção de novas aplicações, que já partem de tal princípio, mas também o seu uso em projetos legados, que já possuem toda uma estrutura constituída.
Para estes casos, onde ainda não há a definição de um modelo de classes de negócio na aplicação, mas sim apenas um banco de dados definido, as tabelas se tornam peças fundamentais para o início de uma eventual “migração” para o uso do framework. Neste cenário, um ponto bastante comum é a presença de tabelas com identificadores compostos (chave primária composta), em que um registro não é identificado por um único valor (ex: Id), mas sim por vários.
Tomando como base o banco Employee que acompanha o Firebird, nele existe uma tabela denominada Job, que estabelece uma Primary Key com três campos: JOB_CODE, JOB_GRADE e JOB_COUNTRY, conforme mostra a imagem a seguir (Figura 8). O detalhe aqui fica por conta do campo JOB_COUNTRY, que refere-se a uma chave estrangeira (Foreign Key) associada à tabela COUNTRY.
Em um caso como este, tendo em mente a adoção do Aurelius, a simples transformação de uma chave tripla para uma chave única poderia acarretar uma série de problemas indesejáveis a uma aplicação já previamente estruturada e em funcionamento. Pensando nisso, o framework dispõe de recurso para o mapeamento de classe com identificador composto. Na verdade, não se trata de uma marcação exclusiva ou especial, mas sim a possibilidade de se mapear vários campos como Id, tal como mostrado na Listagem 6
[Entity]
[Table('COUNTRY')]
[Id('FCountry', TIdGenerator.None)]
TCountry = class
private
[Column('COUNTRY', [TColumnProp.Required], 15)]
FCountry: string;
[Column('CURRENCY', [TColumnProp.Required], 11)]
FCurrency: string;
public
property Country: string read FCountry write FCountry;
property Currency: string read FCurrency write FCurrency;
end;
[Entity]
[Table('JOB')]
[Id('FJobCode', TIdGenerator.None)]
[Id('FJobGrade', TIdGenerator.None)]
TJob = class
private
[Column('JOB_CODE', [TColumnProp.Required], 5)]
FJobCode: string;
[Column('JOB_GRADE', [TColumnProp.Required])]
FJobGrade: Integer;
[JoinColumn('JOB_COUNTRY', [TColumnProp.Required])]
FJobCountry: TCountry;
public
property JobCode: string read FJobCode write FJobCode;
property JobGrade: Integer read FJobGrade write FJobGrade;
property JobCountry: TCountry read FJobCountry write FJobCountry;
end;
Nesta codificação de exemplo, primeiramente é apresentado o mapeamento da classe TCountry, que ocorre de forma bastante tradicional. Sua exposição aqui serve apenas para facilitar o entendimento do conceito apresentado. A seguir, a classe TJob é mapeada para uma tabela JOB, tendo dois de seus campos (FJobCode e FJobGrade) marcados como Id. De forma complementar, é feito também o mapeamento de cada um deles para o campo desejado na tabela: JOB_CODE e JOB_GRADE, respectivamente.
Já o terceiro campo identificador, FJobCountry, por se tratar de uma FK, reflete-se no contexto da classe como sendo um elemento do tipo TCountry. Neste ponto, um novo conceito se estabelece, relacionado ao atributo utilizado para mapeá-lo.
JoinColumn
Em ocasiões como esta o atributo JoinColumn é então utilizado para indicar o campo que será usado na associação, representando a coluna “chave estrangeira” da tabela. Já no seu lado prático, no exemplo mostrado ele foi declarado com dois argumentos, ambos referentes à coluna relacionada na tabela do banco. O primeiro nada mais é do que o nome da coluna, conforme já visto em outras exemplificações. Já o segundo, TColumProp, é um tipo que especifica determinada propriedade à coluna, conforme mostrado a seguir:
- TColumnProp.Required: indica que a coluna é de preenchimento obrigatório, o tradicional NOT NULL de banco de dados;
- TColumnProp.Unique: indica que os valores para esta coluna devem ser únicos, para tal, um índice Unique Key será criado na tabela do banco de dados;
- TColumnProp.NoInsert: indica que o valor deste campo não será persistido no banco de dados em situações de inserção (insert);
- TColumnProp.NoUpdate: indica que o valor deste campo não será persistido no banco de dados em situações de atualização (update).
Manipulação da estrutura de banco de dados
TDatabaseManager é a classe presente no framework responsável por manipular a estrutura de um banco de dados. Aqui entenda manipular como sendo o processo de criação ou atualização estrutural baseado no seu modelo de classes. Para isso são utilizados basicamente dois métodos: BuildDatabase e UpdateDatabase. Uma vez chamado, o primeiro irá executar as instruções SQL necessárias para a criação de uma estrutura de banco de dados que seja condizente com a estrutura de classes do projeto mapeadas como entidade. Em termos de código, o trecho exibido na Listagem 7 exemplifica seu uso. O detalhe aqui fica por conta de seu construtor, que recebe como parâmetro um objeto do tipo IDBConnection que, conforme já visto, representa a conexão com o banco de dados no contexto do Aurelius. Sem deixar de mencionar, seu uso requer a declaração da unit Aurelius.Engine.DatabaseManager.
var
MyDBManager: TDatabaseManager;
begin
MyDBManager := TDatabaseManager.Create(MyConnection);
MyDBManager.BuildDatabase;
MyDBManager.Free;
end;
Já o método UpdateDatabase atua de uma forma um pouco mais complexa, uma vez que sua própria ação exige uma maior profundidade de detalhes. O ponto aqui é que o framework tem que manter a estrutura existente íntegra, ao mesmo tempo em que aplica as novas mudanças necessárias. Para isso ele trabalha em duas etapas. Na primeira ocorre um processo de validação de estruturas (schemas), onde se compara o schema atual da aplicação com o schema do banco de dados já existente. A diferença, caso haja, dá origem então ao script SQL necessário para a atualização. Seguindo por este caminho, a segunda etapa se resume na efetiva execução deste script no banco, tornando-o condizente com a estrutura presente na aplicação. Em complemento aos métodos BuildDatabase e UpdateDatabase está o método DestroyDatabase que se refere à exclusão da estrutura do banco de dados existente.
Manipulação de objetos
A manipulação de objetos diz respeito às operações CRUD (Create-Read-Update-Delete) que, num contexto de aplicações de banco de dados, podem ser traduzidas basicamente nas instruções de Insert, Select, Update e Delete, respectivamente.
Object Manager
Object Manager é o elemento provido pelo TMS Aurelius para a manipulação dos objetos da aplicação. Ele é implementado por meio da classe TObjectManager, presente em Aurelius.Engine.ObjectManager, que é então a unit que deverá ser declarada no código do projeto para o seu uso. Em termos conceituais, ele atua como uma camada entre a aplicação e o banco de dados, provendo os métodos necessários para as operações CRUD.
Devido à própria robustez do framework, vários são os métodos disponibilizados neste cenário, usuais a vários contextos diferentes. Em vista disso, a seguir são explicitados alguns dos principais.
Save
O método Save pode ser considerado o mais básico dentre os disponíveis, uma vez que sua função é a de inserir dados dentro do banco de dados. Para tal, ele então manipula os objetos de entidades da aplicação, persistindo suas informações em uma ou mais tabelas. Tendo como base o exemplo mostrado até aqui, uma chamada a Save para se persistir um objeto Revista se daria da mesma forma que a apresentada na Listagem 8.
var
MyRevista: TRevista;
MyObjectManager: TObjectManager;
begin
// Objeto Revista
MyRevista := TRevista.Create;
MyRevista.Assunto := 'Delphi';
MyRevista.TituloCapa := 'Fique por dentro do TMS Aurelius';
MyRevista.Edicao := 100;
// Object Manager
MyObjectManager := TObjectManager.Create(
TFireDacFirebirdConnection.CreateConnection);
MyObjectManager.Save(MyRevista);
MyObjectManager.Free;
end;
Olhando pelo lado prático, neste momento é importante ressaltar que a instanciação de um objeto do tipo TObjectManager se dá de forma semelhante a um TDatabaseManager, já visto anteriormente. Assim, ao seu construtor (Create) deve-se passar um parâmetro de conexão (IDBConnection), conforme a seguir:
MyObjectManager := TObjectManager.Create(MyConnection);
Voltando ao código apresentado, das linhas 5 a 9 é criado manualmente um objeto denominado MyRevista do tipo TRevista. A seguir, na linha 12 o ObjectManager entra em ação, recebendo como parâmetro em seu construtor a conexão definida no início deste artigo por meio do Wizard. Por fim, na linha 14 o método Save é chamado, ocorrendo então a persistência do objeto no banco de dados, conforme pode ser visto na Figura 9.
Update
O método Update também é utilizado para persistir informações, atualizando os dados de um objeto existente no banco de dados. Funcionalmente, ele se encarrega então de atualizar o devido registro na tabela do banco, baseado no valor da propriedade chave primária do objeto envolvido. Sua chamada segue o mesmo padrão do Save:
MyObjectManager.Update(MyRevista);
SaveOrUpdate
SaveOrUpdate é o método que, conforme seu próprio nome sugere, faz um mix entre os métodos Save e Update. Sendo assim, se o objeto passado contém um identificador válido, indicando uma atualização, um Update é realizado, caso contrário, o Save é acionado. Um exemplo de sua chamada é mostrado a seguir:
MyObjectManager.SaveOrUpdate(MyRevista);
Remove
O método Remove é um complemento aos métodos Save e Update, uma vez que fica responsável por remover o objeto do contexto da aplicação, bem como excluir o registro relacionado no banco de dados, tendo como base o seu identificador:
MyObjectManager.Remove(MyObject);
FindAll
FindAll é o método provido para recuperar todas as instâncias de objeto de uma determinada classe. Num comparativo com o aspecto tradicional de banco de dados, ele funciona como uma espécie de “Select * from Tabela”. Seguindo pelo contexto do framework, uma chamada a FindAll faz com que uma lista de objetos seja retornada (TObjectList):
MyObjects := MyObjectManager.FindAll<TRevista>;
Find<T>
Ao contrário de FindAll, o método Find é designado para a busca de objetos, tomando como base o uso de critérios (criteria no jargão de frameworks ORM). Novamente fazendo uma comparação com o cenário tradicional de aplicações de banco de dados, o método Find seria algo como um Select com Where. A seguir temos uma, dentre as várias possíveis formas de sua chamada:
MyObject := MyObjectManager.Find<TRevista>(Id);
O método Flush
Ainda sobre a manipulação de objetos, o framework conta ainda com outros métodos que podem ser tidos como “complementares” num estudo inicial, mas que se tornam essenciais à medida que sua experiência com o framework aumenta. Em síntese, uma vez que é chamado o método Flush, ele irá persistir todos os objetos pendentes de atualização que estão sob a alcunha do Aurelius naquele momento.
Nesta situação, diferente do método Save, onde um único objeto é passado como argumento para ser persistido, aqui uma única chamada a Flush fará com que um ou vários objetos sejam salvos no banco. Perceba que esse tipo de recurso contribui no atendimento dos mais variados cenários apresentados por uma aplicação de banco de dados. De forma ilustrativa, o trecho de código da Listagem 9 apresenta seu uso. Na situação apresentada, com o uso do Flush, tanto o objeto MyRevista1 quanto o objeto MyRevista2 terão sua coluna “Edição” atualizada no banco de dados.
// Object Manager
MyObjectManager :=
TObjectManager.Create(
TFireDacFirebirdConnection.CreateConnection);
// Objeto Revista 1
MyRevista1 := MyObjectManager.Find<TRevista>(1);
MyRevista1.Edicao := 10;
// Objeto Revista 2
MyRevista2 := MyObjectManager.Find<TRevista>(2);
MyRevista2.Edicao := 11;
// Flush
MyObjectManager.Flush;
MyObjectManager.Free;
Neste código, a linha 2 é crucial para o seu funcionamento, onde temos a instanciação de um objeto TObjectManager, o que é utilizado nas linhas 7 e 11, por meio de seu método Find, para recuperar os objetos desejados. Finalmente na linha 15 é feita uma chamada a Flush, que fará a atualização dos dois objetos no banco de dados.
Um alerta a se fazer com o uso do Flush é com relação à sua performance, que pode ser degradada caso o número de objetos gerenciados pelo Manager seja muito grande. Isto porque, internamente o método faz uma varredura por todas as entidades atreladas ao ObjectManager, verificando quais delas necessitam ser persistidas. Logo, quanto mais objetos, mais tempo é demandado ao processo.
Sem deixar de citar, o método Flush possui ainda uma sobrecarga cuja finalidade se aproxima do método Save, uma vez que é direcionado à persistência de um único objeto, conforme mostrado a seguir:
MyObjectManager.Flush(MyRevista);
TAureliusDataset
Único componente trazido pelo TMS Aurelius em sua instalação, o TAureliusDataset nada mais é do que um componente de ligação de dados (Data Binding) descendente do tradicional TDataSet. Isso o torna compatível com os diversos controles data-aware do Delphi, presentes tanto na VCL quanto na FMX (FireMonkey). Assim, por meio de seu uso é que podemos ligar os objetos oriundos das entidades do Aurelius a esses controles visuais que irão compor as telas da aplicação.
SetSourceList e SetSourceObject
Assim como qualquer outro componente descendente de TDataSet, o TAureliusDataSet também necessita de uma fonte de dados (data source) associada, tornando então estes dados disponíveis para manipulação e visualização. Para isso, ele disponibiliza dois métodos, nomeados sugestivamente como SetSourceList e SetSourceObject, respectivamente. O primeiro é usado para os casos onde se quer associar uma Lista de objetos ao DataSet, enquanto que o segundo associa apenas um único objeto independente. Apenas como ilustração, a seguir são mostrados exemplos de chamadas a SetSourceList e SetSourceObject:
MyAureliusDataSet.SetSourceList(MyRevistas);
MyAureliusDataSet.SetSourceObject(MyRevista);
Indo além com TMS Aurelius
Atendidas as expectativas com as aplicações de banco de dados para VCL e FireMonkey, o Aurelius vai além e proporciona usabilidade também em cenários distribuídos. Para a comunidade, um bom exemplo de distributed applications são as aplicações DataSnap (BOX 1), que se caracterizam por prover um meio comum (Servidor de Aplicação) de acesso a recursos e serviços a clientes de qualquer tipo, seja ele escrito nativamente em Delphi, Java, PHP, JavaScript, etc.
Logo, para que os dados trafeguem entre os lados (Server e Client), torna-se necessária a utilização de uma notação comum, tal como o formato JSON. Para estes casos, o framework conta com classes de conversão tanto de objeto para JSON quanto de JSON para objeto. Didaticamente elas são tratadas como classes Serializer e Deserializer. Como forma de exemplificação, novamente tendo como base um objeto do tipo Revista, sua serialização para JSON se daria da mesma forma que a apresentada na Listagem 10.
var MySerializer: TDataSnapJsonSerializer;
begin
...
MySerializer:= TDataSnapJsonSerializer.Create;
MyJsonValue := MySerializer.ToJson(MyRevista);
...
end;
Anteriormente conhecida como MIDAS, DataSnap é uma tecnologia da Embarcadero que permite a criação de aplicativos multicamadas de forma totalmente RAD. Para tal a IDE traz uma gama de componentes prontos, cada atendendo cada aspecto da arquitetura, incluindo conectividade TCP/IP, HTTP e HTTPS, suporte a REST, autenticação, encriptação e compressão de dados, entre outros recursos.
Historicamente, no contexto do Delphi, o uso da POO na construção de uma aplicação de banco de dados é algo bastante raro. Basicamente dois são os principais fatores que levam a isso. O primeiro está diretamente relacionado ao nível de excelência RAD provido pela ferramenta, através do uso de bibliotecas como dbExpress, FireDac, entre outras. Já o segundo fator, e talvez o mais importante, se dá pela ausência de um framework ORM nativo, que incentive o desenvolvedor a adotá-lo de forma prática e facilitada. Diante disso, o TMS Aurelius vem para preencher justamente esta lacuna, através de sua eficiência e, principalmente, sua facilidade de uso.
Neste artigo você pôde conferir como é fácil entender a mecânica de uso do framework e, conforme a prática, essa facilidade se transpõe para o dia-a-dia da codificação. Código mais enxuto, portabilidade e foco na regra de negócio são alguns dos fatores positivos que sua adoção irá proporcionar.
Talvez o ponto chave aqui não seja a escolha pela continuidade de uma prática já tradicional ou a adoção de uma totalmente nova, mas sim a abertura de sua mente. Em suma, a dica que fica é: pense fora da caixa, se exponha a novos conceitos, aplique-os e se desenvolva internamente. Com a programação não há limites, apenas você. Espero que tenham gostado do artigo e nos vemos na próxima. Bons desenvolvimentos!
Links
- Página oficial do Delphi
- Página de download da versão de avaliação (Trial) do Delphi Berlin 10.1
- Página de download da versão gratuita da ferramenta Delphi 10.1 Berlin Starter Edition
- Página do TMS Aurelius
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Revista