Antigamente, quando o Delphi estava a anos-luz de outras plataformas (hoje felizmente a distancia já diminuiu para alguns meses-luz) eu sempre dizia que primeiro a Embarcadero tinha que implementar coisas como RTTI e generics para que só então o Delphi começasse a ganhar alguns brinquedos legais que existem por exemplo em Java e .Net, “brinquedos” como bons frameworks ORM que não interfiram demasiadamente no design das classes de domínio. Dito e feito, hoje temos o DORM que já foi objeto de um post aqui e, mais recentemente, o TMS Aurelius.
Licenciamento e instalação
Infelizmente o Aurelius é uma ferramenta comercial, diferentemente do DORM, mas achei o seu custo bem acessível. Uma licença para um desenvolvedor sai por 156 euros até o dia 29/02, que na cotação de hoje (18/01/2011) dá aproximadamente R$ 356,00
Além disso, também é possível baixar um trial para experimentar. No trial a maioria das units não vem com os fontes, mas todas as funcionalidades do framework estão disponíveis. Sobre quanto por quanto tempo o trial permanece funcional, não encontrei nada a respeito.
A instalação é muito simples, é só baixar e executar um instalador NNF e assunto encerrado.
Documentação
Em relação à documentação, o Aurelius parece ser muito bem documentado. O pacote acompanha um PDF de 90 páginas que parece cobrir com um bom nível de detalhamento cada aspecto do framework.
Conectividade
Nesta versão, o Aurelius pode se conectar com DB2, Firebird, Interbase, SQL Server, MySQL, NexusDB, Oracle, PostgreSQL e SQLite, através de diferentes suites de componentes, como por exemplo AnyDac, dbExpress, SQLDirect, etc. Além disso, a documentação alega que o framework pode ser facilmente extendido para implementar suporte a outros SGBD e/ou componentes de acesso.
Um detalhe que achei bem interessante é que a documentação do Aurelius lista exatamente quais versões de SGBD e dos componentes de acesso foram utilizadas nos testes, bem como quais combinações entre eles. É apenas um detalhe, mas me passou uma boa sensação de profissionalismo por tras do framework.
Classes de domínio e mapeamento
Como eu já disse na introdução, o Aurelius interfere bem pouco no design das suas classes.
Quando falo sobre interferir no design das classes, me refiro a quando um framework exige que as suas classes herdem de alguma classe ou implementem alguma interface do framework, que as propriedades persistentes sejam todas published, ou que ao invés de usar os tipos pre-definidos do Delphi você tenha que utilizar o próprio sistema de tipos do framework.
Por exemplo, esta classe está pronta para o Aurelius:
type
[Entity]
[Automapping]
TPerson = class
private
FId: integer;
FLastName: string;
FFirstName: string;
FEmail: string;
public
property Id: integer read FId;
property LastName: string read FLastName write FLastName;
property FirstName: string read FFirstName write FFirstName;
property Email: string read FEmail write FEmail;
end;
O atributo Automapping é o que diz ao framework para utilizar convenções de mapeamento naquela classe, mas na minha opinião o comportamento deveria ser o contrário: O mapeamento por convenções ficaria sempre habilitado e, caso o programador por algum motivo não queira isso, então adiciona um atributo como por exemplo [NoAutomapping]. Mas isso é um detalhe menor.
De qualquer forma, como a documentação deixa bem claro, o mapeamento por convenções não é “tudo ou nada”, você pode usar o [Automapping] e ainda assim mapear algumas coisas explicitamente, sobreescrevendo as convenções.
O mapeamento manual, isto é, sem utilizar as convenções, pelo que entendi só pode ser feito via atributos. Ou seja, até onde pude ver não há mapeamento por arquivo externo como acontece no DORM ou no Hibernate. Eu provavelmente não usaria este recurso, mas ainda assim acho que é algo importante de se ter. Algumas pessoas preferem não utilizar atributos para não poluir o código, para que as classes de domínio fiquem menos acopladas ao ORM, ou por terem algum outro impedimento.
Especialmente em se tratando do Delphi, há ainda um outro problema: É que devido á forma como as DCU são geradas e tratadas internamente, se por um acaso você alterar qualquer coisa na interface de uma unit e recompila-la, então você precisará recompilar qualquer unit que dependa desta (Este é o motivo por que componentes compilados para uma versão do Delphi não servem em outra versão). Então modificar uma unit para adicionar os atributos de mapeamento pode gerar um pouco de transtorno em alguns casos mais específicos.
Herança
O Aurelius suporta o mapeamento de herança e queries polimórficas. A herança pode ser mapeada de duas formas:
- Em uma única tabela. Neste caso, todos os campos de todas as subclasses ficam mapeados numa mesma tabela, e há ainda uma coluna extra que permite especificar qual a classe correspondente a cada registro desta tabela.
- Com tabelas específicas para as subclasses, realizando joins quando necessário
ID
Ao que me parece, o Aurelius não tem muitas restrições sobre o identificador dos objetos (a chave primária para o mundo relacional), os campos podem ser de diversos tipos e você pode ter mais de um campo como id na classe, para o caso de chaves compostas.
Ele pode ainda tratar chaves sequenciais automaticamente, utilizando sequences/generators ou campos auto-incremento dependendo do SGBD, mas pelo que observei esta é a única opção para geração automática de identificadores. E também não vi nenhum meio para que eu possa implementar meu próprio gerador de identificadores. No projeto que estou trabalhando atualmente, tenho tabelas cuja chave é um GUID, então neste caso eu mesmo terei que gerar e atribuir o id manualmente sempre que for persistir um objeto para estas tabelas.
Blob
Campos blob são suportados utilizando o tipo TBlob ou TArray nas classes. Acho que TStream seria uma boa opção aqui, possui várias vantagens sobre um TArray e evita que as classes de domínio tenham mais um ponto de acoplamento (TBlob) com o ORM.
Acredito que deve ter havido algum motivo (indisponibilidade de recursos [tempo, dinheiro] ou algum impedimento técnico) para isso, pois acho improvavel que o uso de streams como blobs não tenha passado pela cabeça de ninguém da equipe.
Lazy
No meu post sobre o DORM eu disse que existem alguns impedimentos técnicos para implementar lazy loading de forma completamente transparente em Delphi (O Hibernate consegue, mas envolve manipulação de bytecode), e acho que o pessoal da TMS conseguiu chegar o mais próximo possível. Veja o exemplo:
type
TMediaFile = class
private
[Association([TAssociationProp.Lazy], [])]
[JoinColumn('ID_ALBUM', [])]
FAlbum: Proxy<TAlbum>;
function GetAlbum: TAlbum;
procedure SetAlbum(const Value: TAlbum);
public
property Album: TAlbum read GetAlbum write SetAlbum;
end;
implementation
function TMediaFile.GetAlbum: TAlbum;
begin
Result := FAlbum.Value;
end;
procedure TMediaFile.SetAlbum(const Value: TAlbum);
begin
FAlbum.Value := Value;
end;
Quando um objeto TMediaFile é carregado, o atributo Album não é carregado automaticamente. Ele só será carregado quando a propriedade Album for acessada (Mais especificamente, quando a propriedade Value do Proxy for acessada).
Entretanto, acho que este Proxy poderia ter algum outro nome que deixe explicito para quem estiver lendo a classe que ele está lá para permitir que determinado atributo seja lazy.. Talvez LazyProxy<>
Bugs
Ao executar o demo MusicLibraryVCL, tudo funcionou perfeitamente num primeiro momento. Então decidi compilar o projeto com o FastMM e descobri que há um bug relacionado a gerenciamento de memória, ao que parece envolvendo um objeto sendo liberado da memória duas vezes. Se o problema é no Aurelius ou neste demo em específico, não sei dizer.
Também encontrei um vazamento de memória ao tentar implementar um pouco de código utilizando Aurelius.
Se alguem quiser reproduzir, este foi o código que utilizei:
type
[Entity]
[Automapping]
TAddress = class
private
FId: Integer;
FStreet: String;
FCity: String;
public
property City: String read FCity write FCity;
property Street: String read FStreet write FStreet;
end;
[Entity]
[Automapping]
TPerson = class
private
FId: Integer;
FName: String;
FAddress: TAddress;
public
property Name: String read FName write FName;
property Address: TAddress read FAddress;
end;
...
var
Con: IDBConnection;
DM: TDatabaseManager;
OM: TObjectManager;
P: TPerson;
begin
ReportMemoryLeaksOnShutdown := True;
if FileExists('Data.bin') then
DeleteFile('Data.bin');
Con := TSQLiteNativeConnectionAdapter.Create('Data.bin');
DM := TDatabaseManager.Create(Con);
try
DM.BuildDatabase;
finally
DM.Free;
end;
OM := TObjectManager.Create(Con);
try
P := TPerson.Create;
P.Free;
finally
OM.Free;
end;
end;
Já reportei ambos os casos para o Wagner Landgraf, responsável pelo Aurelius.
Update: Segundo o Wagner, o primeiro caso é uma falha especifica do demo em questão, e não do Aurelius propriamente dito, mas que obviamente será corrigida de qualquer forma.
E o segundo caso de fato é um memory leak que surge quando se utiliza automapping com relacionamentos, a causa do problema já foi identificada e o leak foi corrigido. Devo dizer que gostei da velocidade com que a situação foi tratada!
Queries
Para executar queries, a única opção é o uso da API de Criteria. Uma outra opção (não suportada pelo Aurelius) seria o uso de OQL (Object Query Language, algo como um SQL para objetos), como a HQL do Hibernate.
Um exemplo de uso da API de criteria do Aurelius é o seguinte:
var
Results: TObjectList<TCustomer>;
begin
Results := Manager1.CreateCriteria<TCustomer>
.Add(TExpression.Eq('Name', 'Mia Rosenbaum'))
.List;
Uma situação onde acho Criteria excelente é quando você tem filtros dinamicos definidos pelo usuário. Você teria algo assim:
var
Criteria: TCriteria<TCustomer>;
Results: TObjectList<TCustomer>;
begin
Criteria := Manager1.CreateCriteria<TCustomer>;
if chFiltrarPorNome.Checked then
Criteria.Add(TExpressions.Eq('Name', EdNome.Text));
if chFiltrarPorCidade.Checked then
Criteria.Add(TExpressions.Eq('City', 'EdCidade.Text));
Results := Criteria.List;
Já para a maioria dos casos, eu iria preferir fazer algo como:
var
Results: TObjectList<TCustomer>;
begin
Criteria := Manager1.ExecuteQuery<TCustomer>('SELECT c FROM Customer c WHERE c.Name = "Mia Rosenbaum"');
Eu penso que criteria e OQL ambos tem o seu espaço no mundo, cada um podendo ser utilizado em determinada situação, e espero que em uma futura versão isso possa ser implementado.
Entretanto, sei que é um trabalho complexo: Envolve todo um trabalho de definir uma linguagem de consulta (provavelmente um sql adaptado), parser para esta linguagem, análise sintatica, tradução para sql nativo, etc.
Também seria interessante a possibilidade de executar queries diretamente em SQL. Você perde um pouco de abstração, mas em alguns casos onde você tenha um gargalo de performance, isto poderia ser a salvação.
Considerações finais
Ainda há alguns pontos para serem melhorados e, embora eu não tenha realizado testes mais profundos envolvendo grafos de objetos mais complexos ou análises de performance, o Aurelius me parece ser uma ferramenta promissora e madura para uso comercial.
Update 19/01/2011:
- Fui informado que alguns dos pontos que levantei aqui já estavam sendo cogitadas, incluindo mapeamento por arquivos, TStream para blobs e mais opções de geração de identificadores (guid e outros).
- Reescrevi a seção sobre queries para explicar melhor a api de criteria
1,203 Views Filed under:
Desenvolvimento, Reviews by magnomp