msdn06_capa.JPG

Clique aqui para ler todos os artigos desta edição

 

Mapeamento Objeto-Relacional com o GenericDataAccess

por Fabio Hemylio

Este artigo apresenta uma implementação prática de um modelo de objetos utilizado para prover um mecanismo de mapeamento de objeto-relacional simples e, além disso, visa esclarecer que se trata de um assunto complexo e extenso. O conteúdo abordado aqui permite desenvolver aplicações orientadas a objetos que sejam fáceis de manter e estender. Por esse motivo, vale a pena o esforço para projetar uma camada de acesso a dados/persistência. Este artigo se baseia em alguns aspectos do desenvolvimento de aplicações:

 

- Aplicações OO e Bancos Relacionais: é o padrão de mercado das novas aplicações;

- Somente o ADO.NET não provê o nível de abstração adequado para a camada de persistência;

- Aplicações OO precisam de uma boa camada de persistência de objetos;

- Deixar instruções SQL fixas no código-fonte não é boa idéia porque sua aplicação fica “amarrada” ao esquema do banco de dados.

 

Enquanto os bancos de dados orientados a objetos não se tornam competitivos com os bancos de dados relacionais devido à complexidade para representar diferentes tipos de relacionamentos entre objetos, precisamos nos preocupar com algo que não é tão simples de definir e projetar: o mapeamento objeto-relacional (OR).

Um dos principais desafios para o projetista da camada de acesso a dados de qualquer aplicação orientada a objetos consiste na maneira como fazer o mapeamento OR. Há vários artigos disponíveis sobre o assunto, escritos por renomados gurus da programação orientada a objetos, mas nenhum deles consegue abranger todos os casos de mapeamento exigidos no dia-a-dia das aplicações.

Uma opção recomendada seria implementar uma “Classe Sombra” (Shadow Classes) para cada objeto de negócio, que seria responsável pelo mapeamento dos objetos entre a Aplicação OO e o BD Relacional. Essa classe implementará as 4 operações básicas de banco de dados para cada classe de negócio da sua aplicação: Insert, Select, Update e Delete. No entanto, essa estratégia tem um pequeno problema: o volume de código a ser desenvolvido. Imagine que numa grande aplicação você tenha 300 classes que representem seus objetos de negócio; seriam necessárias mais 300 “classes sombra” com lógica de mapeamento para o banco de dados relacional.

Na próxima versão do Framework .NET, teremos a opção de usar o ObjectSpace, que promete agilizar bastante o desenvolvimento desses componentes de acesso a dados. Enquanto essa nova funcionalidade não chega, resolvi implementar uma versão simplificada do ObjectSpace. Chamei esta implementação de GenericDataAccess e, como o próprio nome diz, ela torna “genérico” o acesso a dados relacionado ao mapeamento OR. Em outras palavras, não é preciso associar uma “classe sombra” a cada objeto de negócio, pois o GenericDataAccess faria esse trabalho por todos eles.

O GenericDataAccess, assim como outras estratégias de mapeamento OR, não resolve todos os casos de mapeamento, e sim os mais simples, onde existe o relacionamento 1/1 (1 classe do aplicativo é mapeada para 1 tabela no banco), que é o caso mais comum nas aplicações. Por exemplo, ela não resolve o problema de mapeamento de objetos mais complexos para mais de uma tabela ou que envolvam consultas que relacionam as tabelas.

O funcionamento do GenericDataAccess se baseia na descoberta em tempo de execução das informações necessárias para gerar automaticamente instruções Select, Insert, Update e Delete. Essas informações necessárias seriam: nomes de tabelas, nomes de colunas, tipo de dados de cada coluna e se uma coluna é chave-primária ou auto-incremento. As instruções geradas variam de acordo com o valor dessas informações de mapeamento, que são obtidas a partir de um documento XML (veja a Listagem 1). O uso deste “miniframework” reduz drasticamente a quantidade de linhas de código requeridas para inserir um objeto numa tabela do banco de dados. Veja um exemplo da classe Produto na Listagem 2.

 

code01.jpg

 

Listagem 2 Classe Produto

public class Produto

{

  private int codigoProduto;

  private string nomeProduto;

                       

  public Produto(int NewCodigoProduto,

                 string NewNomeProduto)

  {

    nomeProduto = NewNomeProduto;

    codigoProduto = NewCodigoProduto;

  }

 

  public int CodigoProduto

  {

    get{return codigoProduto;}

    set{codigoProduto = value;}

  }

 

  public string NomeProduto

  {

    get{return nomeProduto;}

    set{nomeProduto = value;}

  }

}

 

Uso do GenericDataAccess

A classe GenericDataAccess possui somente membros de classe (estáticos). A propriedade Connection representa um objeto do tipo SqlConnection que será utilizado para executar as instruções geradas. Essa propriedade deve ser configurada antes da execução de qualquer operação, ou será gerada uma exceção. A propriedade MapObject representa a estrutura de dados que armazena as informações de mapeamento carregadas através do método Load. O caminho do arquivo XML que contém as informações de mapeamento deve ser informado como parâmetro desse método. A quantidade de código necessária para realizar uma operação de banco de dados em um objeto fica reduzida a uma linha de código. O código para inserir um novo produto no banco de dados ficaria assim:

 

//Conexão c/ o BD

GenericDataAccess.Connection = cnn;

//Carrega informações

GenericDataAccess.MapObject.Load("MapFile.xml");

Produto p = new Produto(1, "Queijo Mussarela");

GenericDataAccess.ExecuteInsertCommand(p);

 

Se alguma propriedade de sua classe for mapeada para uma coluna “Auto - Increment ” no banco de dados, o valor dessa propriedade será ignorado no momento da inserção e será preenchido com o valor retornado do banco (@@Identity) depois da inserção. A mesma sintaxe é utilizada para os métodos ExecuteUpdateCommand e ExecuteDeleteCommand.

Novamente, é importante lembrar: o intuito do GenericDataAccess não é prover um mecanismo de geração automática de qualquer tipo de consultas no banco de dados, mas somente as instruções básicas necessárias ao mapeamento de objetos. O uso desse componente não substitui o uso direto do ADO.NET para executar consultas mais complexas, tais como as que envolvem mais de uma tabela, mas fornece um certo nível de flexibilidade que atende a muitos casos. Exemplos:

 

1.     Retornar do banco todos os objetos do tipo Produto:

GenericDataAccess.Connection = cnn;

GenericDataAccess.MapObject.Load("MapFile.xml");

 

object[] array = GenericDataAccess.ExecuteSelectCommand(typeof(Produto))

 

foreach(object o in array)

{

Produto p = (Produto)o;

  Console.WriteLine("Código: {0} // Nome: {1}",

                    p.CodigoProduto,

                    p.NomeProduto);

}

 

2.     Obter todos os Produtos de acordo com critérios de seleção (comparativo de igualdade):

GenericDataAccess.Connection = cnn;

GenericDataAccess.MapObject.Load("MapFile.xml");

 

Hashtable where = new Hashtable(5);

where["CodigoProduto"] = 1;

where["NomeProduto"] = "Queijo Mussarela";

 

object[] array = GenericDataAccess.ExecuteSelectCommand(typeof(Produto)

,where)

 

foreach(object o in array)

{

  Produto p = (Produto)o;

  Console.WriteLine("Código: {0} // Nome: {1}",

                    p.CodigoProduto,

                    p.NomeProduto);

}

 

O método ExecuteSelectCommand espera que os critérios de seleção sejam informados através de um Hashtable, onde a chave (key) é uma string com o nome da propriedade da classe pela qual se quer aplicar o critério de seleção. O valor de busca é o valor associado a essa chave no Hashtable. Para cada elemento desse Hashtable, será adicionada uma condição de seleção na cláusula WHERE da instrução SELECT a ser gerada. Se houver mais de um critério, eles serão associados ao operador lógico AND dentro da cláusula WHERE. O operador de comparação default é o de igualdade (=), ou seja, a cláusula WHERE será gerada para o banco de dados com comparações de igualdade entre a propriedade e o valor informado. Caso seja necessário usar outro operador de comparação, recomenda-se um objeto do tipo WhereParameter, que veremos a seguir.

 

3.     Obter todos os Produtos de acordo com critérios de seleção (comparativos diversos):

GenericDataAccess.Connection = cnn;

GenericDataAccess.MapObject.Load("MapFile.xml");

 

Hashtable where = new Hashtable(5);

where["CodigoProduto"] = new WhereParameter(WhereOperator.GreaterThan,10);

where["NomeProduto"] = new WhereParameter(WhereOperator.Like, "Q%");

 

object[] array = GenericDataAccess.ExecuteSelectCommand(typeof(Produto)

,where)

 

foreach(object o in array)

{

  Produto p = (Produto)o;

  Console.WriteLine("Código: {0} // Nome: {1}",

                    p.CodigoProduto,

                    p.NomeProduto);

}

 

A enumeração WhereOperator fornece a lista dos operadores de comparação a serem utilizados na geração da cláusula WHERE. Durante a construção do objeto WhereParameter, é passado um valor para o WhereOperator e, em seguida, é passado o valor que será utilizado para montar o critério de seleção. Também é possível fazer com que os objetos sejam retornados em ordem ascendente ou descendente por meio de algumas de suas propriedades.

 

4.     Obter todos os Produtos em ordem descendente de NomeProduto:

 

GenericDataAccess.Connection = cnn;

GenericDataAccess.MapObject.Load("MapFile.xml");

 

string[] orderBy = {"NomeProduto"};

 

object[] array = GenericDataAccess.ExecuteSelectCommand(typeof(Produto),null,

orderBy, false);

 

foreach(object o in array)

{

  Produto p = (Produto)o;

  Console.WriteLine("Código: {0} // Nome: {1}",

                    p.CodigoProduto,

                    p.NomeProduto);

}

 

Uma outra sobrecarga do método ExecuteSelectCommand aceita que se informe um array de strings contendo os nomes das propriedades pelas quais os objetos serão ordenados. Um outro parâmetro booleano informa se os objetos estarão em ordem ascendente (default, valor do parâmetro: true) ou em ordem descendente (valor do parâmetro: false).

Além dos métodos ExecuteInsertCommand, ExecuteUpdateCommand, ExecuteDeleteCommand e ExecuteSelectCommand, há também os métodos GetInsertCommand, GetUpdateCommand, GetDeleteCommand e GetSelectCommand. Eles fornecem um objeto tipo SqlCommand que contém a instrução a ser executada, com todos os parâmetros já configurados e preenchidos com os valores informados. Caso seja necessário alterar alguma coisa na instrução a ser executada, use esses métodos.

O uso do GenericDataAccess pode ajudar o desenvolvedor a economizar muitas linhas de código no processo de construção de sua camada de acesso a dados. Ao fornecer acesso genérico, ele poderá implementar atividades rotineiras mais facilmente. Lembre-se no entanto de que, no caso de mapeamentos mais complexos, ainda será necessário o uso direto do ADO.NET.

Esse modelo deve ser avaliado e também customizado para garantir o seu uso de acordo com as necessidades de cada aplicação.

Conclusão

Agora você pode aplicar o conteúdo do GenericDataAccess aprendido neste artigo. Na minha equipe de desenvolvimento, conseguimos aumentar a produtividade da camada de persistência de novos aplicativos. Analise o código-fonte e, caso seja necessário, modifique-o para atender aos requisitos específicos às suas aplicações. Este modelo vai ser aperfeiçoado constantemente, e espero contar com a ajuda de vocês para isso. A construção do GenericDataAccess é só um pequeno exemplo do que podemos criar utilizando o poder do Framework .Net. Aproveitem!

 

Referências:

Designing Data Tier Components and Passing Through Tiers - http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/boagag.asp

Mapping Objects to Relational Databases - http://www.ambysoft.com/mappingobjects.pdf

 

OLHO: A quantidade de código necessária para realizar uma operação de banco de dados em um objeto é reduzida a uma linha de código.