ADO.NET – Generic Code


Há softwares que desenvolvemos que devem ser independentes de base de dados, ou seja, não se pode prever qual será a base de dados que o cliente terá em sua empresa, pois há muitos em que empresas adotam SQL Server, outras Oracle e, provavelmente, eles não irão adotar uma nova base de dados (que tem um alto custo), apenas para suportar o sistema desenvolvido para você.

Isso leva as fábricas de softwares escreverem o código de acesso aos dados de forma genérica, justamente para poder vender o seu produto e assim, suportar qualquer base de dados e, conseqüentemente, conseguirá ter um maior número de clientes que poderá atender.

Para conseguirmos isso na versão 1.x do ADO.NET, temos que trabalhar com as Interfaces genéricas que estão disponíveis dentro do Namespace System.Data, sendo as principais: IDataAdapter, IDataParameter, IDataParameterCollection, IDataReader, IDbCommand, IDbConnection, IDbDataAdapter e IDbTransaction. Essas interfaces são implementadas pelas classes concretas, como por exemplo a classe SqlClient e OracleClient e, sendo assim, conseguimos devolver para quem invoca o método, uma instância da classe concreta da base de dados que estamos utilizando. Um exemplo de código genérico para trabalhar com as Interfaces é mostrado abaixo:

public IDbConnection GetConnection(){
    string provider = GetProviderFromConfig();
    IDbConnection conn = null;

    switch(provider){
        case "SQLServer":
            conn = new SqlConnection(); break;
        case "Oracle":
            conn = new OracleConnection(); break;            
    }

    return conn;
}

Conforme mostrado no código acima, conseguimos chegar onde queremos, ou seja, termos um acesso genérico à qualquer base de dados, desde que seu provider implementa essas interfaces. Mas há um grande problema nessa forma de termos genericidade no acesso à base de dados. Como as interfaces são públicas e também imutáveis, temos um grande problema quando for preciso (por design), adicionar um novo membro nessa interface. Como elas são imutáveis, não se pode adicionar um novo membro, pois quebraria todo o “contrato” com as classes que as implementam. Sendo assim, esses novos membros devem ser adicionados nas classes concretas, perdendo assim as funcionalidades que o mesmo irá fornecer, já que não irão estar presentes na Interface.

Passamos por esse problema na transição do ADO.NET 1.0 para o ADO.NET 1.1. Na versão 1.0 não existia uma propriedade para verificar se há ou não registros em um DataReader. Em alguns casos, conseguimos manipular essa deficiência com o método Read, mas ainda assim temos problemas quando tentamos atribuir o DataReader aos containers de dados. Na versão 1.1 do ADO.NET, a Microsoft decidiu adicionar uma propriedade chamada HasRows, a qual retorna um valor boleano indicando se há ou não registros, mas essa propriedade não foi adicionada na Interface IDataReader, mas sim nas classes concretas: SqlDataReader, OracleDataReader e OleDbDataReader.

Já no ADO.NET 2.0, isso foi reestruturado e agora temos o que chamamos de Base Classes. As Interfaces que vimos anteriormente ainda existem e são implementadas nessas Base Classes. Essas Base Classes passam a ser herdadas pelos objetos concretos (exemplo: SqlConnection e SqlCommand) e lá são implementados os métodos de acordo com o provider específico. Esse design é ideal para possibilitar a adição de uma nova funcionalidade no futuro, garantindo assim que, se adicionarmos uma nova funcionalidade em uma classe base, ela poderá ou não ser implementanda na sua classe concreta e com a vantagem de não quebrar o contrato de implementação, que é impossível com o uso de Interfaces. Abaixo é mostrado como ficou a nova estrutura das classes de acesso aos dados do ADO.NET 2.0, desde as Interfaces genéricas, as Base Classes e as classes concretas:

Interface Base Class Classe Concreta *
IDbConnection DbConnection SqlConnection
IDbCommand DbCommand SqlCommand
IDataReader/IDataRecord DbDataReader SqlDataReader
IDbTransaction DbTransaction SqlTransaction
IDbDataParameter DbParameter SqlParameter
IDataParameterCollection DbParameterCollection SqlParameterCollection
IDbDataAdapter DbDataAdapter SqlDataAdapter
  DbCommandBuilder SqlCommandBuilder
  DbConnectionStringBuilder SqlConnectionStringBuilder
  DBDataPermission SqlPermission

* Apenas defini as classes do Namespace SqlClient para o exemplo, mas as heranças das classes base se extendem para os Namespaces OracleClient, OleDb, Odbc e você pode ainda customizar caso desejar, desde que herde da classe base e implemente os métodos e propriedades de acordo com a sua base de dados e também crie o provider factory, que veremos a seguir.

O código que vimos um pouco acima para exemplificar a escrita de código genérico à base de dados que utiliza Interfaces, é agora reescrito utilizando as Base Classes:

public DbConnection GetConnection(){
    string provider = GetProviderFromConfig();
    DbConnection conn = null;

    switch(provider){
        case "SQLServer":
            conn = new SqlConnection(); break;
        case "Oracle":
            conn = new OracleConnection(); break;            
    }

    return conn;
}

Provider Factories

O código que vimos acima retorna a instância de um objeto XXXConnection específico de acordo com o provider que temos definido no arquivo *.Config, pois lá temos uma chave que indica o provider (SQL, Oracle ou OleDb) que iremos utilizar na aplicação, já que devemos, via código, retornar a instância do objeto concreto. Isso nos fará recompilar a aplicação, caso um novo provider seja adicionado/utilizado pela mesma pois, no caso acima, devemos adicionar uma nova condição para o Select/switch.

Para suprir essa necessidade e termos “alguém” quem crie a instância do objeto concreto, temos também dentro do Namespace System.Data.Common uma classe abstrata chamada DbProviderFactory, que contém vários métodos que devem ser sobrescritos nas classes concretas, retornando os objetos específicos de cada provider. Isso quer dizer que, para cada Provider, temos uma classe responsável por retornar uma instância dos objetos concretos (para SQL Server temos a classe SqlClientFactory e para Oracle a classe OracleClientFactory). Estas classes retornam conexões, os comandos, entre outros. Para melhor explicar, vejamos a tabela e a imagem a seguir:

Método (DbProviderFactory) Tipo de Retorno Instância (SqlClient) Instância (OracleClient)
CreateConnection DbConnection SqlConnection OracleConnection
CreateCommand DbCommand SqlCommand OracleCommand
CreateCommandBuilder DbCommandBuilder SqlCommandBuilder OracleCommandBuilder
CreateConnectionStringBuilder DbConnectionStringBuilder SqlConnectionStringBuilder OracleConnectionStringBuilder
CreateDataAdapter DbDataAdapter SqlDataAdapter OracleDataAdapter
CreateParameter DbParameter SqlParameter OracleParameter
CreatePermission CodeAccessPermission SqlClientPermission OraclePermission

Figura 1 – Estrutura hierárquica das factories.

Como já sabemos, as classes concretas, como por exemplo o SqlCommand, herdam diretamente da classe DbCommand, logo, já conseguimos ter o acesso genérico, pois os métodos sempre retornam instâncias dos objetos concretos, e podemos nos certificar disso vendo a tabela acima. Mas e quem se encarrega de criar a instância da classe XXXClientFactory (responsável por retornar os objetos específicos de cada Provider)? Temos uma classe chamada DbProviderFactories, a qual possui três métodos e entre eles, o método GetFactory, que dado um nome de um provider (que geralmente será definido na ConnectionString no arquivo de configuração da aplicação), retorna a instância do objeto XXXClientFactory. Vejamos como fica a ConnectionString, informando o Provider a ser utilizado:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add 
         name="SQLServer" 
         connectionString=
             "integrated security=SSPI;data source=localhost;initial catalog=DB" 
         providerName="System.Data.SqlClient" />
    
    <add 
         name="Access" 
         connectionString=
             "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:DB.mdb;User Id=;Password=;" 
         providerName="System.Data.OleDb" />
  </connectionStrings>
</configuration>

Notem que para acessar o banco de dados SQL Server, utilizamos o provider System.Data.SqlClient e para utilizarmos o banco de dados Microsoft Access, utilizamos o provider System.Data.OleDb. Agora só resta descobrir onde ele faz a amarração e, para esclarecer, isso fica definido no arquivo machine.config, como é mostrado na imagem abaixo:

Figura 2 – Factories definidas no arquivo machine.config.

Para exemplificarmos isso, iremos recuperar os dados de duas bases diferentes, mas com o mesmo código, apenas alternando as strings de conexão para acesso. O cenário será recuperar os dados de uma base de dados dentro de um servidor SQL Server e também de dados provenientes de um arquivo do Microsoft Access. O código abaixo, com o uso do código genérico, fará com que utilizaremos o mesmo código para retornar os dados de ambas as base de dados:

private void btnListar_Click(object sender, EventArgs e) {
    ConnectionStringSettingsCollection connStrings = 
        ConfigurationManager.ConnectionStrings;

    for (int i = 1; i < connStrings.Count; i++) {
        ConnectionStringSettings cs = connStrings[i];
        DbProviderFactory factory =
            DbProviderFactories.GetFactory(cs.ProviderName);

        DbConnection conn = factory.CreateConnection();
        conn.ConnectionString = cs.ConnectionString;
        DbCommand cmd = factory.CreateCommand();
        cmd.CommandText = "SELECT * FROM Cliente";
        cmd.Connection = conn;

        DbDataReader reader = null;
        try
        {
            conn.Open();
            reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);

            if (reader.HasRows)
            {
                while (reader.Read()) {
                    this.lstClientes.Items.Add(reader.GetString(1));
                }
            }
            else
            {
                MessageBox.Show("Não há registros.");
            }
        }
        catch
        {
            MessageBox.Show("Ocorreu um erro.");
        }
        finally
        {
            if (reader != null) reader.Close();
        }
    }
}

Como resultado, a imagem abaixo exibe as duas base de dados (Access e SQL Server) com a tabela clientes aberta, podendo se visualizar os dados e, em seguida, a imagem com um formulário Windows, já com um controle ListBox, populado com os dados das duas tabelas de banco de dados diferentes:

Figura 3 – As tabelas de ambas as bases de dados preenchidas.

Figura 4 – Aplicativos Windows consumindo estes dados.

Somente para finalizar, a única coisa que devemos nos atentar é com relação a estrutura da base de dados, ou seja, as tabelas, seus campos e tipos de dados devem ser iguais entre todas as bases de dados utilizadas, para que não ocorram erros que provavelmente somente serão detectados em tempo de execução da aplicação. Outra coisa que também não foi abordada é a utilização do provider OleDb para acesso genérico às bases de dados, pois como já é de conhecimento da maioria dos desenvolvedores, utilizando-o com uma base de dados do tipo SQL Server (a qual já tem provider desenhado para o mesmo), perde muito em performance, e não é aconselhável utilizá-lo para este fim.

Claro que este não é o cenário ideal, pois em uma aplicação somente trabalharemos com uma base de dados, mas o importante é observar que utilizamos o mesmo código para a mesma finalidade, porém buscando dados de bases completamente diferentes.

ADONET20.zip (118.08 kb)

Anúncios

4 comentários sobre “ADO.NET – Generic Code

  1. Preciso utilizar parâmetros independente do banco de dados também;
    No caso em SQL
    SqlParameter[] sqlParams = new SqlParameter[] {
    new SqlParameter("@username", strUserName) ,
    new SqlParameter("@password", strPassword)
    };

    e neste caso se fosse em um banco de dados oracle ocorreria erro.
    Como eu resolvo isso ?

    • Olá israel,
      venho acompanhado seu site e artigos a algum tempo e estou tentando fazer um metodo que retone a StringConnection e ProviderName de uma tag contida no app.config, o problema é que por mais que eu adicione a StringConnection nas propriedades do projeto ou direto escrevendo no app.config o array connStrings, a seguir, só fica com 1 valor !

      public string DBConfig(string nomeConexao)
      {
      ConnectionStringSettingsCollection connStrings =
      ConfigurationManager.ConnectionStrings;

      ConnectionStringSettings cs = connStrings[nomeConexao];
      //connStrings : items count 1 : _key = ‘LocalSqlServer’
      return cs.ProviderName;
      }
      Obs: Já referenciei o System.Configuration.dll
      e estou passando a string correta, até por que já tentei utilizar: connStrings[1] e sempre retorna null

      Tenho de adicionar a connectionString em outro Lugar ?
      Ou fazer mais alguma coisa ?
      Não é recomendavel utilizar as configurações do app.config ?

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s