Framework de Abstração para Bancos de Dados - Parte 1 - Introdução
E aí galera!!! Como vocês estão?
Como eu havia dito em meu último post no meu site do Multiply, irei publicar aqui uma série de artigos falando (e dando exemplos, lógico!) de um framework (sim, acho que poderei chamá-lo assim hehe) que que construí para abstrair operações em banco de dados em bibliotecas de classe, onde ele é utilizado. Ainda não tenho um nome para ele, lá onde eu trabalho ele geralmente é chamado de "Classe de Conexão do Leonel", pura e simplesmente :-)
Ele é construído com base no .NET Framework 2.0 e suporta bancos de dados Firebird (usando o .NET Firebird Client), SQL Server (usando SQL Client), Oracle (usando Oracle Client da MS), bancos de dados acessíveis via OLE DB e ODBC, ou seja, qualquer banco de dados, sendo que alguns com provider nativo, como o Firebird e o SQL Server.
Como assim abstrair operações de bancos de dados?
Durante o desenvolvimento de bibliotecas de classe, geralmente precisamos fazer acesso a um banco de dados para consultar, inserir e atualizar os registros dentro dele. Para isso, no .NET utilizamos as classes presentes no ADO.NET e dos diversos providers que são suportados por ele.
Cada provider do ADO.NET possui diversas classes que representam os objetos que fazem o acesso ao banco, como Connection, DataReader, DataAdapter, Transaction, entre outros.
Estes objetos que citei são interfaces dentro do ADO.NET, como o Connection que é a interface IDBConnection do ADO.NET. Cada provider implementa estas interfaces. Você já reparou que os providers tem os mesmos objetos, só mudando o nome? Sim, é isso mesmo! Se você aprende a utilizar um, pode utilizar todos. Claro que cada um tem as suas particularidades, mas vamos ficar com o que tem no padrão.
Por exemplo, se quisermos executar uma instrução INSERT no SQL Server e no Firebird, teremos o seguinte:
SQL Server:
1: public bool Inserir()
2: {
3: bool retorno = false;
4: SQLConnection cn = new SQLConnection(this.MinhaStringDeConexao);
5: SQLCommand cd = new SQLCommand();
6: try
7: {
8: cd.Connection = cn;
9: cn.Open();
10: cd.CommandText = "insert into MINHATABELA values (1,2,3)";
11: cd.ExecuteNonQuery();
12: }
13: catch
14: {
15: retorno = false;
16: }
17: finally
18: {
19: cn.Close();
20: cn.Dispose();
21: cd.Dispose();
22: }
23: return retorno;
24: }
Firebird:
1: public bool Inserir()
2: {
3: bool retorno = false;
4: FbConnection cn = new FbConnection(this.MinhaStringDeConexao);
5: FbCommand cd = new FbCommand();
6: try
7: {
8: cd.Connection = cn;
9: cn.Open();
10: cd.CommandText = "insert into MINHATABELA values (1,2,3)";
11: cd.ExecuteNonQuery();
12: }
13: catch
14: {
15: retorno = false;
16: }
17: finally
18: {
19: cn.Close();
20: cn.Dispose();
21: cd.Dispose();
22: }
23: return retorno;
24: }
Viu como é a mesma coisa? Só muda os nomes dos objetos, do ponto de vista da nossa codificação.
Mas convenhamos que ficar instanciando estes objetos em cada método em que temos que executar um comando SQL é um pé no saco. Lembrando ainda que sempre temos que fechar as conexões que abrimos e desalocar da memória os objetos que instanciamos! Ficar repetindo estes códigos (instancia pra lá, fecha conexão pra cá, finaliza pra cá...) leva tempo, e como a velha máxima para os profissionais de TI de que "tempo é dinheiro" é bem válida, temos que economizar tempo para entregar rapidamente o projeto, senão, já viu!
A minha proposta com este framework de abstração é justamente "pular" esta parte de instanciar e destruir objetos de banco de dados. O controle da conexão e dos objetos de BD é feito única e exclusivamente dentro da classe principal do framework; nas nossas classes, iremos nos preocupar unicamente com as regras de negócio.
Ele é composto de uma classe mais algumas enumerações auxiliares que visa a execução de comandos SQL e recuperação de valores em banco de dados baseado em instruções select que retornam datatables ou object. Dentro desta classe há métodos e propriedades que abstraem todos os objetos mais utilizados de acesso a dados, portanto, não é necessário conhecer os objetos especificos de cada provider, apenas é necessário fazer a referência do mesmo no seu projeto.
Um pouco de história - Como foi o "pontapé" inicial:
No ano de 2005, fiz um curso de ASP.NET com C#, e nele aprendi a fazer páginas ASP.NET, trabalhar com bibliotecas de classe e acessar banco de dados.
No curso teve uma aula em que o professor falou sobre herança. Na superclasse ficavam os objetos de conexão a banco de dados, como o Connection e tinha algums métodos para executar comando SQL, como o Inserir() que coloquei como exemplo. Na subclasse, tinha uma rotina que montava um comando SQL e que chamava esse método da superclasse para executar o comando SQL. A partir daí, comecei a desenvolver os projetos onde eu trabalho seguindo esta metodologia: em uma classe ficavam os métodos que executavam comandos SQL, e nas subclasses tinham as regras de negócio. Nas páginas ASPX ou nos formulários Windows não tinha código de banco de dados, somente chamadas às classes contidas na biblioteca de regras de negócio.
Inicialmente, construí a minha "Classe de Conexão" somente com os objetos do Firebird, que é o banco de dados que utilizamos nos projetos. Esta classe tinha métodos para executar comandos que não retornam resultsets (como um INSERT ou um UPDATE), executar stored procedure, retornar resultsets nas formas de DataTable e FbDataReader e algumas propriedades para guardar a string de conexão, mensagens para o usuário, entre outras.
Projetos e mais projetos com o Firebird a classe ia evoluindo junto. Melhorias no tratamento de erros, novos métodos, otimização dos métodos existentes... muita coisa foi feita, mas ainda tinha um problema: era fortemente dependente do FireBird! Nos métodos das subclasses, para executar uma stored procedure, por exemplo, era necessário declarar cada FbParameter! E os DataReaders? Era necessária a utilização de vários FbDataReaders em várias partes do código!
Em um dos projetos em que eu trabalho, teve problema de lentidão entre outros. Além do servidor inadequado para a aplicação, ela não estava bem escrita, confesso. Revisei todo o código da mesma, e claro, fiz uma melhoria drástica na "Classe de Conexão": Objetos ESPECÍFICOS do FireBird ficam APENAS na "Classe de Conexão"! Nas subclasses, eu nem colocaria a cláusula using referente ao provider do mesmo. Reescrevi todos os métodos das classes para se adequarem ao novo formato (isso fora a migração do Framework 1.1 para o 2.0). Quem controla abertura e fechamento de conexão, e gerenciamento dos objetos de banco de dados instanciados é a Classe de Conexão. Resultado: código mais limpo e mais rápido também. Valeu muito a pena, embora tenha me dado um estresse danado :-)
Um dia, um colega de serviço, o famoso Beto (como eu sempre digo - NÃO ESTOU ZUANDO, hehehe - , esse é O CARA!!!), me propôs um desafio: "duvido você construir uma classe que acesse vários bancos de dados", mais ou menos assim. Dito e feito! Construí a classe que faz acesso a vários bancos de dados! Os providers específicos de cada BD ficam somente na classe de conexão, e as referências ao provider na cláusula using também. Fora da classe, não é utilizado NENHUM objeto específico de banco de dados!
A versão atual do framework roda em projetos com Oracle, Firebird e SQL Server sem nenhuma mudança no código. É a mesmíssima classe nestes projetos.
A mágica? A Orientação a Objetos (POO) é a mágica! Se vocês olharem o código da classe que irei disponibilizar, eu uso e abuso das interfaces das quais as classes do provider implementam. Conforme o provider a ser utilizado, eu atribuo o objeto específico ao genérico que eu declarei e executo a funcionalidade. Quer um exemplo?
Supomos que exista uma interface ISerHumano e duas classes THomem e TMulher que o implementam. Então, é possível fazer o seguinte:
1: public interface ISerHumano
2: {
3: public void Falar();
4: }
5:
6: public partial class THomem : ISerHumano
7: {
8: public void Falar()
9: {
10: this.AlgumaCoisa();
11: }
12: }
13:
14: public partial class TMulher : ISerHumano
15: {
16: public void Falar()
17: {
18: this.OutraCoisa();
19: }
20: }
21:
22: public void Testar()
23: {
24: ISerHumano pessoa1;
25: ISerHumano pessoa2;
26: pessoa1 = new THomem(); //Sim, é possível atribuir um objeto em um outro objeto de classe ou interface ascendente!
27: pessoa2 = new TMulher();
28: pessoa1.Falar(); // -> Irá executar o método implementado na classe THomem
29: pessoa2.Falar(); // -> Irá executar o método implementado na classe TMulher
30: }
Sim, embora o objeto pessoa1 seja declarado como a interface, quando atribuímos-lhe uma instância de THomem, ao chamarmos o método Falar() será executado o método implementado na classe THomem, e quando atribuímos-lhe uma instância de TMulher, será executado o método implementado na classe TMulher.
Com isso, o retorno dos métodos são dos tipos básicos (ou das interfaces) da linguagem ou do namespace System.Data, ou seja, booleanos, inteiros, objects, datatables. Nenhum objeto específico do BD é retornado por alguma função deste framework.
Ele é utilizado em bibliotecas de classes, portanto, forçando o desenvolvimento em camadas: um projeto de apresentação (um site ASP.NET ou um Formulário Windows) e pelo menos uma biblioteca de classes que irá conter as regras do negócio. O foco deste framework é esta biblioteca de classes. Ele foi programado para ser utilizado como a superclasse principal das classes que manipulam os dados no banco de dados. Portanto, iremos utilizar largamente o conceito de herança!
Para "forçar" o programador a não utilizar os objetos de execução dos comandos SQL na camada de apresentação, todos os métodos que pedem SQL como parâmetro são declarados como protected. Portanto, é necessário uma subclasse para utilizá-los; não é possível instanciar diretamente a classe de conexão.
O download da classe de conexão poderá ser feito aqui: Página de Suporte do NeoMatrix Tech
Classe de Conexão (versão 1) (7 KiB)
Nos próximos artigos, desenvolveremos uma aplicação de cadastro utilizando este framework.
Então, até lá e um forte abraço a todos!
