Veja neste artigo como criar um framework de persistência em Delphi.

Para o desenvolvimento de softwares, atualmente, a arquitetura mais disseminada é a multicamadas. Geralmente divida em três camadas: banco de dados, camada de negócios e interface (apresentação) (Figura 1).

 Arquitetura multicamadas
Figura 1. Arquitetura multicamadas

O fator complicador nesse modelo de arquitetura está no acesso aos dados que é realizada pela camada de negócios através do acesso nativo a camada de dados. Para minimizar esta situação, nos deparamos com a necessidade de intercalar nessas camadas uma camada de persistência de dados (Figura 2).

Arquitetura multicamadas com camada de persistência de dados
Figura 2. Arquitetura multicamadas com camada de persistência de dados

As operações básicas da camada de persistência são conhecidas como operações de CRUD (Create, Retrieve (ou Read), Update e Delete). Assim sendo, a camada de persistência deve realizar estas operações de forma transparente a aplicação e conseqüentemente livrar a camada de negócios das preocupações relativas ao acesso a dados.

Portanto, o objetivo deste artigo é justamente mostrar como os desenvolvedores que trabalham com arquitetura multicamadas, podem criar o seu próprio framework de persistência objeto-relacional em Delphi XE, levando suas aplicações a um nível de abstração de dados necessário a produtividade do desenvolvimento.

Criando as Classes de Entidades

Quando se pensa em camada de persistência primeiramente se pensa em criar as classes de entidades. E quem seriam estas classes? Seriam exatamente espelhos das entidades, ou tabelas, do esquema da base de dados usado na aplicação. Para ilustrar a criação do framework de persistência objeto-relacional, primeiramente será necessário criar uma tabela no Firebird (Listagem 1).

 
CREATE TABLE TB_CAD_ALUNO (
  MATRICULA   VARCHAR(10) NOT NULL,
  NOME_ALUNO  VARCHAR(100) NOT NULL,
  ENDERECO    VARCHAR(200),
  TELEFONE    VARCHAR(20),
  CPF         VARCHAR(11)
);
ALTER TABLE TB_CAD_ALUNO ADD CONSTRAINT TB_CAD_ALUNO_PK PRIMARY KEY (MATRICULA);
Listagem 1. Script de criação da tabela TB_CAD_ALUNO

Depois de criada a tabela, cria-se um novo projeto no Delphi XE para começar a implementar os artefatos necessários a camada de persistência. Assim sendo:

Crie um novo projeto no Delphi XE do tipo VCL Forms Applications. Salve o projeto. Crie junto com os fontes deste projeto uma pasta chamada Framework. Esta será a pasta onde os fontes necessários ao Framework de persistência serão arquivados, objeto de estudo deste artigo (Figura 3).

Exemplo da árvore do projeto
Figura 3. Exemplo da árvore do projeto

Usa-se o conceito de RTTI (Run-time Type Information) [2] para criar as classes de entidades. Assim sendo, adicione uma nova Unit ao seu projeto e dê a ela o nome de TEntity.pas, salve-a dentro da pasta Framework e implemente-a de acordo com a Listagem 2. Esta classe será a classe genérica para qualquer classe de entidade presente em qualquer esquema de banco de dados.


unit TEntity;

interface

Uses RTTI;

type
  TGenericEntity = class(TObject)
  end;

implementation


end.
Listagem 2. TEntity.pas

Agora crie a classe de entidade referente à tabela TB_CAD_ALUNO (Listagem 1). Para tal crie uma nova Unit e salve-a dentro da pasta Framework como tblAluno.pas e implemente-a de acordo com a Listagem 3.


unit tblAluno;

interface

Uses  TEntity;

type
//nome da classe de entidade
  TAluno = class(TGenericEntity)
  private
    //colocar aqui todas as propriedades privadas da classe de acordo com
    //os campos da tabela
    FMatricula:string;
    FNomeAluno:string;
    FEndereco:string;
    FTelefone:string;
    FCPF:string;

  //colocar aqui todos os métodos sets para cada uma das propriedades acima
    procedure setFMatricula(value:string);
    procedure setFNomeAluno(value:string);
    procedure setFEndereco(value:string);
    procedure setFTelefone(value:string);
    procedure setFCPF(value:string);

  public
    //colocar aqui todas as propriedades públicas de acesso aos objetos dessa
    //classe
    property Matricula: string read FMatricula write setFMatricula;

    property Nome:string read FNomeAluno write setFNomeAluno;

    property Endereco:string read FEndereco write setFEndereco;

    property Telefone:string read FTelefone write setFTelefone;

    property CPF:string read FCPF write setFCPF;

    function ToString:string; override;
end;

implementation

procedure TAluno.setFMatricula(value:string);
begin
  FMatricula:= value;
end;

procedure TAluno.setFNomeAluno(value:string);
begin
  FNomeAluno:= value;
end;

procedure TAluno.setFEndereco(value:string);
begin
  FEndereco:= value;
end;

procedure TAluno.setFTelefone(value:string);
begin
  FTelefone:= value;
end;

procedure TAluno.setFCPF(value:string);
begin
  FCPF:= value;
end;


function TAluno.toString;
begin
  result := '''' Matricula: ''''+ Matricula + '''' Nome: ''''+ Nome +
  '''' Endereco: '''' + Endereco +
  '''' Fone: '''' + Telefone + '''' CPF: '''' + CPF;
end;


end.
Listagem 3. TblAluno.pas

Agora é necessário customizar a classe TAluno usando conceitos de Custom Attributes [3]. O que será realizado é simplesmente colocar Custom Attributes para cada elemento da classe TAluno de forma que seja possível tirar proveito deles através da RTTI e criar o DAO (Data Access Object).

Primeiro crie os Custom Attributes necessários para identificar dentro da classe de entidade a tabela do banco de dados e seus campos. Crie, portanto, uma nova Unit, chamada de CAtribEntity e salve-a dentro de Framework, implementando-a conforme a Listagem 4.


unit CAtribEntity;

interface

//atributo para determinar o nome da tabela na entidade a ser usada
type //nome da tabela
  TableName = class(TCustomAttribute)
  private
    FName: String;
  public
    constructor Create(aName: String);
    property Name: String read FName write FName;
  end;

type //determinar se o campo é um campo chave
  KeyField = class(TCustomAttribute)
  private
    FName: String;
  public
    constructor Create(aName: String);
    property Name: String read FName write FName;
  end;

type  //nome do campo na tabela
  FieldName = class(TCustomAttribute)
  private
    FName: String;
  public
    constructor Create(aName: String);
    property Name: String read FName write FName;
  end;


implementation

constructor TableName.Create(aName: String);
begin
  FName := aName
end;

constructor KeyField.Create(aName: String);
begin
  FName := aName;
end;

constructor FieldName.Create(aName: String);
begin
  FName := aName;
end;

end.
Listagem 4. CAtribEntity.pas

Depois customize a classe de entidade TAluno com os Custom Attributes necessários, alterando a implementação da classe conforme a Listagem 5. Dessa forma, tem-se neste exato momento o mapeamento objeto-relacional usando para isso os Custom Attributes.


unit tblAluno;

interface

Uses  TEntity,  CAtribEntity;

type
//nome da classe de entidade
  [TableName(''''TB_CAD_ALUNO'''')]
  TAluno = class(TGenericEntity)
  private
    //colocar aqui todas as propriedades privadas da classe de acordo com
    //os campos da tabela
    FMatricula:string;
    FNomeAluno:string;
    FEndereco:string;
    FTelefone:string;
    FCPF:string;

  //colocar aqui todos os métodos sets para cada uma das propriedades acima
    procedure setFMatricula(value:string);
    procedure setFNomeAluno(value:string);
    procedure setFEndereco(value:string);
    procedure setFTelefone(value:string);
    procedure setFCPF(value:string);

  public
    //colocar aqui todas as propriedades públicas de acesso aos objetos dessa
    //classe
    [KeyField(''''MATRICULA'''')]
    [FieldName(''''MATRICULA'''')]
    property Matricula: string read FMatricula write setFMatricula;

    [FieldName(''''NOME_ALUNO'''')]
    property Nome:string read FNomeAluno write setFNomeAluno;

    [FieldName(''''ENDERECO'''')]
    property Endereco:string read FEndereco write setFEndereco;

    [FieldName(''''TELEFONE'''')]
    property Telefone:string read FTelefone write setFTelefone;

    [FieldName(''''CPF'''')]
    property CPF:string read FCPF write setFCPF;

    function ToString:string; override;
end;

implementation

procedure TAluno.setFMatricula(value:string);
begin
  FMatricula:= value;
end;

procedure TAluno.setFNomeAluno(value:string);
begin
  FNomeAluno:= value;
end;

procedure TAluno.setFEndereco(value:string);
begin
  FEndereco:= value;
end;

procedure TAluno.setFTelefone(value:string);
begin
  FTelefone:= value;
end;

procedure TAluno.setFCPF(value:string);
begin
  FCPF:= value;
end;


function TAluno.toString;
begin
  result := '''' Matricula: ''''+ Matricula + '''' Nome: ''''+ Nome 
  + '''' Endereco: '''' + Endereco +
            '''' Fone: '''' + Telefone + '''' CPF: '''' + CPF;
end;


end.
Listagem 5. tblAluno.pas – alteração usando os Custom Attributes

Criando o DAO

DAO (Data Access Object) é um padrão de desenvolvimento de softwares que trata da persistência de objetos separando a camada de dados da camada de negócios [4]. Para entender melhor a implementação do DAO é necessário conhecer os conceitos de Generics [4, 5]. Este recurso nos permite trabalhar com uma estrutura genérica, o que faz muita diferença em linguagens fortemente tipada como o Delphi, podendo definir-se uma estrutura genérica customizada, ou melhor, tipada de acordo com a necessidade.

Primeiro crie um DataModule com o nome de srvModBaseDados e salve também dentro da pasta Framework. Este DataModule será responsável por fazer a interface entre a camada de dados e o DAO da camada de persistência, ou seja, neste ponto já se cria a primeira independência de projeto em relação ao SGBD utilizado. Coloque TDSServerModuleBaseDados na propriedade name do DataModule. Veja na Figura 4 os componentes visuais necessários ao DataModule.

Data Module: srvModBaseDados
Figura 4. Data Module: srvModBaseDados

Configure o componente TSQLConnection para acesso ao banco de dados criado neste artigo. E depois configure o Componente TSQLDataSet para o respectivo TSQLConnection do DataModule. Depois crie os métodos necessários à conexão com o banco e os métodos necessários ao CRUD (Listagem 6).


unit srvModBaseDados;

interface

uses
  SysUtils, Classes, DBXFirebird, DBXPool, FMTBcd, DB, SqlExpr;

type
  TDSServerModuleBaseDados = class(TDataModule)
    LSCONEXAO: TSQLConnection;
    SQLDSServidor: TSQLDataSet;
  private
    { Private declarations }
  public
    { Public declarations }
    //funções para o banco de dados
    function Conectar:boolean;
    function Desconectar:boolean;

    //funções para manipular as entidades
    function getDataSet(strQry:string): TDataSet;
    function execSql(strQry:string): boolean;
  end;

var
  DSServerModuleBaseDados: TDSServerModuleBaseDados;

implementation

{$R *.dfm}

//funções para o banco de dados
function TDSServerModuleBaseDados.Conectar:boolean;
begin
  try
    LSCONEXAO.Connected := true;
    result := true;
  except
    result := false;
  end;
end;

function TDSServerModuleBaseDados.Desconectar:boolean;
begin
  try
    LSCONEXAO.Connected := false;
    result := true;
  except
    result := false;
  end;
end;


//funções para manipular as entidades
function TDSServerModuleBaseDados.getDataSet(strQry:string): TDataSet;
begin
  SQLDSServidor.Close;
  SQLDSServidor.Params.Clear;
  SQLDSServidor.CommandType := ctQuery;
  SQLDSServidor.CommandText := strQry;
  SQLDSServidor.Open;
  result := SQLDSServidor;
end;

function TDSServerModuleBaseDados.execSql(strQry:string): boolean;
Var
  msgErro:string;
begin
  result := false;
  SQLDSServidor.Close;
  SQLDSServidor.Params.Clear;
  SQLDSServidor.CommandType := ctQuery;
  SQLDSServidor.CommandText := strQry;
  try
    SQLDSServidor.ExecSQL;
    result := true;
  except
   on e: Exception do begin
     raise Exception.Create(e.Message)
   end;
  end;
end;

end.
Listagem 6. srvModBaseDados.pas

Assim sendo, crie outra Unit ao seu projeto, salvando também dentro de Framework com o nome de GenericDao. A Unit GenericDao consolida todos os conceitos de Generics, Rtti, DAO para que seja possível manipular determinada classe de entidade fazendo o mapeamento objeto-relacional (Listagem 7).


unit GenericDao;

interface

Uses Db, Rtti, CAtribEntity, TypInfo, SysUtils, srvModBaseDados;

type
  TGenericDAO = class
    private
      class function GetTableName<T: class>(Obj: T): String;
    public
      //procedimentos para o crud
      class function Insert<T: class>(Obj: T):boolean;
      class function GetAll<T: class>(Obj: T): TDataSet;
  end;


implementation

class function TGenericDAO.GetTableName<T>(Obj: T): String;
var
  Contexto: TRttiContext;
  TypObj: TRttiType;
  Atributo: TCustomAttribute;
  strTable: String;
begin
  Contexto := TRttiContext.Create;
  TypObj := Contexto.GetType(TObject(Obj).ClassInfo);
  for Atributo in TypObj.GetAttributes do
  begin
    if Atributo is TableName then
      Exit(TableName(Atributo).Name);
  end;
end;


//funções para manipular as entidades
class function TGenericDAO.Insert<T>(Obj: T):boolean;
var
  Contexto: TRttiContext;
  TypObj: TRttiType;
  Prop: TRttiProperty;
  strInsert, strFields, strValues: String;
  Atributo: TCustomAttribute;

begin
  strInsert := '''''''';
  strFields := '''''''';
  strValues := '''''''';

  strInsert := ''''INSERT INTO '''' + GetTableName(Obj);

  Contexto := TRttiContext.Create;
  TypObj := Contexto.GetType(TObject(Obj).ClassInfo);

  for Prop in TypObj.GetProperties do begin
    for Atributo in Prop.GetAttributes do begin
        if Atributo is FieldName then begin
           strFields := strFields + FieldName(Atributo).Name  
           + '''','''';
           case Prop.GetValue(TObject(Obj)).Kind of

             tkWChar, tkLString, tkWString, tkString,
             tkChar, tkUString:

               strValues := strValues +
               QuotedStr(Prop.GetValue(TObject(Obj)).AsString) 
               + '''','''';

             tkInteger, tkInt64:

               strValues := strValues +
               IntToStr(Prop.GetValue(TObject(Obj)).AsInteger) 
               + '''','''';

             tkFloat:

               strValues := strValues +
               FloatToStr(Prop.GetValue(TObject(Obj)).AsExtended) 
               + '''','''';

             else
               raise Exception.Create(''''Type not Supported'''');
           end;
       end;
     end;
  end;
  strFields := Copy(strFields, 1, Length(strFields) - 1);
  strValues := Copy(strValues, 1, Length(strValues) - 1);
  strInsert := strInsert + '''' ( '''' + strFields + '''' )  
  VALUES ( '''' + strValues + '''' )'''';

  result := DSServerModuleBaseDados.execSql(strInsert);
end;


class function TGenericDAO.GetAll<T>(Obj: T): TDataSet;
begin
  result := DSServerModuleBaseDados.getDataSet(''''SELECT T1.* '''' +
    '''' FROM '''' + GetTableName(Obj) + '''' T1 '''' );
end;


end.
Listagem 7. GenericDao.pas

Portanto, até este ponto o seu Framework de persistência está criado. No próximo tópico vamos integrá-lo a aplicação Delphi criada no inicio deste artigo. A pasta Framework poderá ser copiada para qualquer aplicativo e usada sem necessidades de reescrita de código, ou poderá ficar em área pública a todos os seus projetos.

Usando a Camada de Persistência

Agora que a camada de persistência está criada cria-se o aplicativo para usá-la. O projeto do aplicativo foi criado no início deste artigo, portanto, será construída uma interface simples apenas para mostrar o uso do framework (Figura 5).

Interface demonstrando o uso da camada de persistência
Figura 5. Interface demonstrando o uso da camada de persistência

Veja a implementação dos botões "Inserir" e "GetAll" (Listagem 8).


procedure TForm5.btnInserirClick(Sender: TObject);
Var
 aluno:TAluno;
begin

  aluno := TAluno.Create();
  aluno.Matricula := edMatricula.Text;
  aluno.Nome      := edNome.Text;
  aluno.Endereco  := edEndereco.Text;
  aluno.Telefone  := edTelefone.Text;
  aluno.CPF       := edCPF.Text;

  if TGenericDAO.Insert(aluno) then begin
    ShowMessage(''''Aluno inserido!'''');
  end else begin
    ShowMessage(''''Aluno não inserido!'''')
  end;

end;

procedure TForm5.btnGetAllClick(Sender: TObject);
begin
  ClientDataSet1.Close;
  DataSetProvider1.DataSet := TGenericDAO.GetAll(TAluno.Create());
  ClientDataSet1.Open;
end;
Listagem 8. Implementação da chamada dos métodos da camada de persistência

Conclusão

Conclui-se portanto que os desenvolvedores devem estar atento as novas implementações e recursos disponíveis em seus ambientes de desenvolvimento. Isso torna o trabalho de produção de softwares não tão árduo e é notória a necessidade cada vez mais inerente desta área em promover as boas práticas no desenvolvimento de softwares.