Вы находитесь на странице: 1из 11

Mapeamento Objeto-Relacional Usando o TMS Aurelius

Desenvolver sistemas com banco de dados um dos usos mais comuns do Delphi. Porm lidar com
comandos SQL pode ser uma tarefa enfadonha. Em geral, ao construirmos um sistema de banco de
dados, o processo geralmente o mesmo:

1. Configurar uma conexo
2. Criar um TSQLQuery ou outro descendente de TDataset que execute comandos SQL
3. Montar o comando SQL e execut-lo via TSQLQuery
4. Abrir o TDataset e varrer os registros lendo os valores usando objetos TField

Tem sido mais ou menos assim desde o incio do Delphi e obviamente um mtodo consagrado - mas
tem suas desvantagens. Temos que montar o SQL manualmente e lembrar os nomes dos campos e
tabelas no banco, tanto na hora de montar a SQL quanto na hora de ler os valores. Se fizermos um
refactor do banco, temos que rever todos os nossos comandos SQL.

Como alternativa, podemos usar uma framework de mapeamento objeto-relacional. Com ela, o
processo diferente: ns fazemos um mapeamento inicial, para informar como cada classe ser salva
no banco de dados e, a partir da, no trabalhamos mais com SQL ou TDataset: manipulamos apenas os
objetos.

O objetivo deste artigo mostrar como usar uma framework de mapeamento objeto-relacional pra
construir aplicaes de bancos de dados. A framework utilizada ser o TMS Aurelius
(http://www.tmssoftware.com/site/aurelius.asp).


Criando uma conexo

A primeira coisa a se fazer configurar uma conexo ao banco de dados. O TMS Aurelius no exige que
voc aprenda quase nada de novo aqui, porque na verdade ele no tem componentes de conexo ao
banco de dados ele simplesmente utiliza os que voc tem na sua aplicao. So disponibilizados
adapters onde voc apenas passa o componente de conexo que voc utiliza. A listagem 1 mostra
como obter uma conexo do Aurelius a partir de uma conexo dbExpress.

Listagem 1 Criando uma conexo
uses
Aurelius.Drivers.Interfaces, Aurelius.Drivers.dbExpress;

var
Conexao: IDBConnection;
begin
Conexao := TDbExpressConnectionAdapter.Create(SQLConnection1, False);
end;

O componente SQLConnection1 um componente dbExpress do tipo TSQLConnection que j existe na
sua aplicao, ou que voc tenha criado s pro Aurelius. l que voc ir configurar sua conexo e, para
us-la no Aurelius, apenas use o adapter pra obter uma interface IDBConnection. Essa varivel Conexao
que criamos na Listagem 1 ser usada em todos os exemplos daqui pra frente.

Existem adapters para vrios tipos de componentes. Se voc no usa o dbExpress e portanto no tem
um componente TSQLConnection, voc pode usar o SQL-Direct (TSDDatabase), AnyDac
(TADConnection), IBX (TIBDatabase), ADO (TADOConnection) e vrios outros.

O Aurelius tambm ir criar os comandos SQL de acordo com os banco de dados ao qual a aplicao est
se conectando. No caso do dbExpress ele detectou automaticamente o tipo de banco com base na
propriedade DriverName do TSQLConnection, mas voc pode especificar explicitamente qual banco
voc est utilizando. As opes so muitas:

Oracle, SQL Server, Interbase, Firebird, MySQL, Nexus, DB2, SQLite, PostgreSQL, entre outros.

Mapeando a classe

Voc pode utilizar uma classe j existente ou criar uma classe nova. O Aurelius possui o recurso de
automapeamento, o que facilita bastante. Veja o exemplo da listagem 2.

Listagem 2 Mapeamento automtico de classe
[Entity]
[Automapping]
TPessoa = class
private
FId: integer;
FNome: string;
FCidade: string;
FTelefone: string;
public
property Id: integer read FId;
property Nome: string read FNome write FNome;
property Cidade: string read FCidade write FCidade;
property Telefone: string read FTelefone write FTelefone;
end;

Na listagem 2, estamos dizendo: essa classepode ser salva no banco (Entity) e deve ser feito um
mapeamento automtico dos campos (Automapping). O Aurelius ir salvar essa classe na tabela
Pessoa, que ter quatro campos: Id, Nome, Cidade e Telefone, o primeiro do tipo integer e os demais
do tipo varchar.

Voc pode definir tudo isso manualmente, usando atributos. Isso pode ser til se voc j tiver uma
aplicao existente, com tabelas j definidas, e quer apenas comear um novo mdulo na aplicao
usando o Aurelius. Nesse caso voc cria sua classe do jeito que voc quer e faz um mapeamento manual
tabela e campos existentes, como no exemplo da listagem 3.

Listagem 3 Mapeamento manual de classe
[Entity]
[Automapping]
[Table(TBL_PESSOAS)]
[Sequence(SEQ_PESSOAS)]
[Id('FId', TIdGenerator.IdentityOrSequence)]
TPessoa = class
Private
[Column('PS_ID', [TColumnProp.Unique, TColumnProp.Required, TColumnProp.NoUpdate])]
FId: integer;
[Column('PS_NOMEPESSOA_A', [TColumnProp.Required], 100)]
FNome: string;
[Column('PS_CIDADE_A', [TColumnProp.Required], 50)]
FCidade: string;
[Column('PS_FONE_A', [TColumnProp.Required], 15)]
FTelefone: string;
public
property Id: integer read FId;
property Nome: string read FNome write FNome;
property Cidade: string read FCidade write FCidade;
property Telefone: string read FTelefone write FTelefone;
end;

Criando o banco de dados Opcional

Voc pode criar as tabelas e campos automaticamente, baseado nas classes que voc definiu e mapeou.
Claro que se voc j tem um banco existente, no precisa realizar essa etapa. A listagem 4 mostra como
criar a estrutura do banco:

Listagem 4 Criando o banco de dados
var
DatabaseManager: TDatabaseManager;
begin
DatabaseManager := TDatabaseManager.Create(Conexao);
try
DatabaseManager.BuildDatabase;
finally
DatabaseManager.Free;
end;
end;

Gerando classes a partir do banco de dados

Como mencionamos, se o banco j existe voc pode apenas criar a classe e mape-la para a tabela e
campos existentes. Pra facilitar esse trabalho, o Aurelius trabalha integrado com a ferramenta Data
Modeler. Essa ferramenta permite que voc importe toda a estrutura do banco de dados para
modelagem e, alm dos recursos de comparao de bancos, gerao de scripts SQL de criao e
atualizao do banco, tambm possui uma opo para gerar automaticamente um cdigo-fonte com
todas as classes j mapeadas, a partir das tabelas existentes.

Figura 1 Opo para criao de classes a partir da estrutura do banco


A gerao de classes bem flexvel voc pode gerar automaticamente a partir das configuraes
padro, ou pode utilizar a janela de configurao pra definir o mapeamento especfico qual classe ser
criada, qual propriedade, etc.. A figura 2 mostra uma parte de um banco de dados importado pelo Data
Modeler, e a listagem 5 um exemplo de uma classe gerada automaticamente, pra tabela Orders.

Figura 2 Tabelas existentes no banco


Listagem 5 Classe mapeada gerada automaticamente pelo Data Modeler
[Entity]
[Table('Orders')]
[Id('FOrderID', TIdGenerator.IdentityOrSequence)]
TOrder = class
private
[Column('OrderID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
FOrderID: integer;
[Column('OrderDate', [])]
FOrderDate: Nullable<TDateTime>;
[Column('ShippedDate', [])]
FShippedDate: Nullable<TDateTime>;
[Association]
[JoinColumn('CustomerID', [], 'CustomerID')]
FCustomer: Proxy<TCustomer>;
[Association]
[JoinColumn('EmployeeID', [], 'EmployeeID')]
FEmployee: Proxy<TEmployee>;
[ManyValuedAssociation([], [TCascadeType.SaveUpdate, TCascadeType.Merge], 'FOrderID')]
FOrderDetailsList: Proxy<TList<TOrderDetails>>;
function GetCustomer: TCustomer;
procedure SetCustomerID(const Value: TCustomers);
function GetEmployee: TEmployee;
procedure SetEmployee(const Value: TEmployee);
function GetOrderDetailsList: TList<TOrderDetails>;
public
constructor Create;
destructor Destroy; override;
property OrderID: integer read FOrderID;
property OrderDate: Nullable<TDateTime> read FOrderDate write FOrderDate;
property ShippedDate: Nullable<TDateTime> read FShippedDate write FShippedDate;
property Customer: TCustomer read GetCustomer write SetCustomer;
property Employee: TEmployee read GetEmployee write SetEmployee;
property OrderDetailsList: TList<TOrderDetails> read GetOrderDetailsList;
end;


Salvando objetos no banco

Uma vez definida a conexo, o mapeamento e, com as tabelas prontas no banco de dados, hora de
comear efetivamente a construir a aplicao. Como mencionado, a partir de agora toda a aplicao fica
baseada em objetos. A listagem 6 mostra como criar uma nova pessoa no banco de dados:

Listagem 6 Salvando um objeto
var
Pessoa: TPessoa;
Manager: TObjectManager;
begin
Pessoa := TPessoa.Create;
Pessoa.Nome := 'Oswaldo';
Pessoa.Cidade := 'Sao Paulo';
Pessoa.Telefone := '(11) 2222-2222';
Manager := TObjectManager.Create(Conexao);
try
Manager.Save(Pessoa);
Manager.Flush;
IdPessoa := Pessoa.Id;
finally
Manager.Free;
end;
end;

At a criao do TObjectManager, o cdigo Delphi puro e simples apenas manipulao de objetos.
Ento criamos um gerenciador (de objetos), o TObjectManager, pra salvar esse objeto no banco. Note
que passamos para o gerenciador a interface Conexao que criamos anteriormente.

O mtodo Save informa que estamos salvando um objeto novo, e o mtodo Flush diz ao gerenciador
para efetuar no banco todas as alteraes que fizemos (no caso, solicitamos um salvamento). O Aurelius
ento ir montar a SQL, setar os parmetros de acordo com as propriedades do objeto, e efeutar o
INSERT na tabela. Se voc est usando um Id do tipo identity ou sequence, voc pode obter o valor
gerado atravs da propriedade Id, conforme o exemplo.

Outra observao importante: note que no destrumos o objeto Pessoa. Isso no um memory leak
uma vez que o objeto passa a ser gerenciado pelo TObjectManager (nesse caso quando chamamos o
Save), ele ser destrudo automaticamente quando no for mais necessrio. Assim podemos usar os
objetos tranquilamente, inclusive as associaes, sem nos preocuparmos em destru-los.


Atualizando objetos

Para atualizar objetos j existentes, basta busc-los, alter-los e chamar o Flush novamente. A listagem
7 mostra como alterar o telefone da pessoa que acabamos de criar.

Listagem 7 Atualizando objetos
var
Manager: TObjectManager;
Pessoa: TPessoa;
begin
Manager := TObjectManager.Create(Connection);
try
Pessoa := Manager.Find<TPerson>(IdPessoa);
Pessoa.Telefone := (11) 1234-1234;
Manager.Flush;
finally
Manager.Free;
end;
end;

interessante notar que o Aurelius detecta apenas as propriedades alteradas (no caso Telefone) e
executa o Update atualizando apenas esse campo. Isso ajuda o desempenho em aplicaes multiusurio
e evita que um usurio sobrescreva o que o outro alterou. O SQL executado seria o seguinte
(considerando o mapeamento manual da listagem 2):

UPDATE TBL_PESSOAS SET PS_FONE_A=:p0 WHERE PS_ID=:p1;


Herana

Um recurso interessante do Aurelius o suporte a herana. Como estamos trabalhando com classes e
objetos, til e sensato que herana, um recurso bsico de orientao a objetos, possa ser utilizada. Por
exemplo, podemos criar as seguintes novas classes (Listagem 8):

Listagem 8 Classes herdadas
[Entity]
[Automapping]
[Inheritance(TinheritanceStrategy.JoinedTables)]
[Table(TBL_PESSOAFISICA)]
TPessoaFisica = class(TPessoa)
private
FCPF: string;
public
property CPF: string read FCPF write FCPF;
end;

[Entity]
[Automapping]
[Inheritance(TinheritanceStrategy.JoinedTables)]
[Table(TBL_PESSOAJURIDICA)]
TPessoaJuridica = class(TPessoa)
private
FCNPJ: string;
public
property CNPJ: string read FCNPJ write FCNPJ;
end;

Nesse caso, o Aurelius ir criar duas novas tabelas, TBL_PESSOAFISICA e TBL_PESSOAJURIDICA, ambas
relacionadas tabela TBL_PESSOA atravs de uma chave estrangeira. A tabela da pessoa fsica ter um
campo CPF e a da jurdica um campo CNPJ. possvel tambm configurar o Aurelius pra salvar todas as
classes em uma mesma tabela. A listagem 9 mostra como salvar uma classe TPessoaJuridica.

Listagem 9 salvando uma classe herdada
var
Pessoa: TPessoaJuridica;
Manager: TObjectManager;
begin
Pessoa := TPessoa.Create;
Pessoa.Nome := 'Embarcadero';
Pessoa.Cidade := 'Sao Paulo';
Pessoa.Telefone := '(11) 1111-1111';
Pessoa.CNPJ := 00.000.000/0001-00;
Manager := TObjectManager.Create(Conexao);
try
Manager.Save(Pessoa);
Manager.Flush;
IdPessoaJuridica := Pessoa.Id;
finally
Manager.Free;
end;
end;

Alguma diferena em relao listagem 6? Nenhuma. A nica diferena que estamos lidando com
uma outra classe, e portanto temos propriedades diferentes no caso, a propriedade CNPJ. O Aurelius
se encarregar de fazer todos os SQLs necessrios. No caso, ele ir executar um INSERT na tabela
TBL_PESSOA, salvando os campos nome, cidade e telefone, obter o Id dessa tabela, e fazer um INSERT
na tabela TBL_PESSOAJURIDICA, salvando o campo cnpj e tambm o Id gerado, pra definir a chave
estrangeira com a tabela TBL_PESSOA.

O interessante dessa abordagem que na hora de desenvolver a aplicao, tudo fica mais fcil
podemos organizar as classes e objetos de acordo com nossa lgica de negcio e toda a gerao
complexa de SQLs fica a cargo do framework.


Associaes

Todo banco de dados tem relacionamentos e assim tambm devemos ter nas classes. Como cada classe
corresponde (geralmente) a uma tabela, os relacionamentos so definidos justamente atravs de
propriedades cujo tipo so classes. Observe a classe definida e mapeada na listagem 10:

Listagem 10 Classes com associaes
[Entity]
[Automapping]
TNotaFiscal = class
private
FId: integer;
FPessoa: TPessoa;
FData: TDateTime;
FItens: TList<TItemNota>;
public
constructor Create;
destructor Destroy; override;
property Id: integer read FId;
property Pessoa: TPessoa read FPessoa write FPessoa;
property Data: TDateTime read FData write FData;
property Itens: TList<TItemNota> read FItemNota write FItemNota;
end;

A classe TNotaFiscal ser mapeada pra uma tabela NotaFiscal. Note que essa classe tem uma
propriedade do tipo TPessoa. Isso significa que a tabela NotaFiscal ter uma chave estrangeira pra
tabela Pessoa (relacionamento muitos-pra-um). E tambm h uma lista de itens (classe TitemNota, que
omitimos na listagem). O Aurelius ir criar uma tabela ItemNota e ir relacionar com a tabela NotaFiscal
(uma nota pode ter muitos itens).

interessante lembrar tambm que, como a nota fiscal est relacionada com uma classe TPessoa, pode-
se utilizar qualquer objeto do tipo TPessoa, e isso inclui objetos do tipo TPessoa Juridica ou
TPessoaFisica. A listagem 11 mostra como seria o salvamento de uma nota fiscal:

Listagem 11 Salvando objetos com associaes
var
Pessoa: TPessoa;
Nota: TNotaFiscal;
ItemNota: TItemNota;
Manager: TObjectManager;
begin
Manager := TObjectManager.Create(Conexao);
try
Nota := TNotaFiscal.Create;
Nota.Data := Date;
Nota.Pessoa := Manager.Find<TPessoa>(IdPessoaJuridica.Id);
ItemNota := TItemNota.Create;
ItemNota.Produto := 'Martelo';
ItemNota.Valor := 10;
Nota.Itens.Add(ItemNota);
Manager.Save(Nota);
Manager.Flush;
finally
Manager.Free;
end;
end;


Pesquisas (Queries)

Finalmente, aps salvar e atualizar os objetos, o que se precisa uma forma de encontr-los no banco.
O Aurelius fornece uma API pra realizar consultas. semelhante ao uso de comandos SQL voc define
os critrios de busca, filtros, ordenao, agrupamento, etc.. A Listagem 12 mostra um exemplo de
consulta.

Listagem 12 Executando uma consulta
var
Manager: TObjectManager;
Notas: TObjectList<TNotaFiscal>;
begin
Manager := TObjectManager.Create(Conexao);
try
Notas := Manager.CreateCriteria<TNotaFiscal>
.CreateAlias('Pessoa', 'p')
.Where(TLinq.GreaterThan('Data', EncodeDate(2010, 1, 1))
and (TLinq.Like('p.Cidade', '%do Sul%')))
.AddOrder(TOrder.Asc('Data'))
.List;
try
// use a lista Notas como quiser
// por exemplo, for Nota in Notas do ...
finally
Notas.Free;
end;
finally
Manager.Free;
end;
end;

No exemplo da listagem 12, estamos buscando todas as notas fiscais cuja data seja maior que
01/01/2010 e tambm onde o nome da cidade da pessoa relacionada nota contenha o texto do Sul.
Solicitamos tambm que as notas sejam trazidas ordenadas por data.

Podemos verificar duas coisas: primeiro, possvel fazer pesquisas pelas associaes (assim como
faramos usando SQL com LEFT ou INNER JOIN). Segundo, a herana tambm tem o seu papel aqui.
Como a nota fiscal pode estar relacionada a pessoa, pessoa jurdica ou pessoa fsica, a busca tambm
deve seguir esse critrio. Portanto, o Aurelius ir executar um SQL que ir fazer a busca nas trs tabelas,
e retornar todas as notas que atendem ao critrio estabelecido, sejam pessoas jurdicas ou fsicas.


Demais recursos

Vale lembrar que existem vrios outros recursos:

- Propriedades Nullable: Voc pode definir propriedades como Nullable (que recebem valores null).

- Carga tardia (lazy-loading): Ao carregar um objeto com associaes (NotaFiscal.Pessoa por exemplo),
pode-se configurar a propriedade Pessoa de modo que o Aurelius s busque os dados da pessoa quando
realmente essa propriedade for acessada.

- Campos Blob: pode-se usar campos blob (binrio, texto) e tambm definir uma carga tardia pra eles (s
trazer o contedo do campo quando necessrio).

- Outros tipos de Id: alm do tipo integer, pode-se usar outros tipos de chave primria como string e
data, bem como chaves compostas (apesar de no ser uma boa prtica!)

- Queries complexas: pode-se definir queries com projections (expresses, campos calculados),
agrupamentos (Sum, Group By) e mesmo setar diretamente a SQL de partes da query, para casos mais
avanados.


Concluso

O objetivo desse artigo foi fazer uma introduo ao mapeamento objeto-relacional, especialmente
usando a framework TMS Aurelius. Vrios outros assuntos podem ser discutidos e apresentados, no que
se refere a construirs aplicaes usando conceitos objeto-relacional, mas esses tpicos esto fora do
escopo desse artigo. Espero que tenha sido interessante e que tenha conseguido apresentar uma forma
diferente de se construir aplicaes de banco de dados utilizando o Delphi.


Autor
Wagner Rafael Landgraf, 35, formado em Engenharia Eletrnica e Mestre em Informtica Industrial
pela Universidade Federal Tecnolgica do Paran. Atualmente Gerente de Produtos da TMS Software
(http://www.tmssoftware.com), trabalha com Delphi desde sua primeira verso em 1995, e
responsvel por produtos como TMS Scripter Studio, TMS Diagram Studio, TMS Data Modeler e TMS
Aurelius.