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)

ADO.NET – Trabalho Assíncrono

Temos nas versões 1.x do ADO.NET três métodos bastante importantes dentro da classe XXXCommand, sendo eles: ExecuteReader (retorna um result-set de dados), ExecuteXmlReader (retorna um result-set em formato XML (XmlReader)) e ExecuteNonQuery (executa uma Transact-SQL statement e retorna um número inteiro que representa o número de linhas afetadas). Todos estes métodos são responsáveis por executar de forma síncrona, bloqueando assim a Thread corrente.

O problema deste comportamento é que temos que aguardar o processamento da base de dados finalizar para que possamos novamente ter acesso operante ao sistema. Isso acaba sendo bastante complicado, já que muitas vezes o processo é demorado, principalmente quando as regras de negócio estão dentro da base de dados. Apesar de se conseguir realizar um processo assíncrono no ADO.NET 1.x com o auxílio de Delegates Assíncronos e da classe ThreadPool, a Microsoft decidiu facilitar nesta versão do ADO.NET a criação de comandos assíncronos na base de dados, onde foi adotada uma metodologia similar ao que temos dentro do .NET Framework, mas até o momento é somente suportado pelo provider SqlClient.

Para estes mesmos métodos, temos também mais dois métodos auxiliares (Begin e End) para suportar chamadas assíncronas. A tabela abaixo mostra os novos métodos:

Síncrono Assíncrono (Begin) Assíncrono (End)
ExecuteNonQuery BeginExecuteNonQuery()
BeginExecuteNonQuery(AsyncCallback, Object)
EndExecuteNonQuery(IAsyncResult)
ExecuteReader BeginExecuteReader()
BeginExecuteReader(AsyncCallback, Object)
EndExecuteReader(IAsyncResult)
ExecuteXmlReader BeginExecuteXmlReader()
BeginExecuteXmlReader(AsyncCallback, Object)
EndExecuteXmlReader(IAsyncResult)

Os métodos BeginXXX têm geralmente duas sobrecargas, onde em uma delas não é passado nenhum parâmetro. Já na outra dois parâmetros são requeridos: um delegate do tipo AsyncCallback que apontará para um procedimento de callback, que será executado quando a operação for finalizada; já o segundo parâmetro trata-se de um objeto que é passado para esse procedimento de callback, que poderá ser recuperado pela propriedade AsyncState. Essa propriedade é acessada através do objeto IAsyncResult (retornado pelo método BeginXXX), o qual é passado como parâmetro para o procedimento de callback e, está definido na assinatura do delegate AsyncCallback.

Como já foi dito, esses métodos retornam um objeto do tipo IAsyncResult que representa o status da operação assíncrona. Este, por sua vez, é passado para o procedimento de callback (quando existir), que provê informações importantes e também necessárias para finalizarmos o processo assíncrono. Já os métodos EndXXX devem ser invocados para completar a operação do processo assíncrono. Em seu parâmetro devemos também passar um objeto do tipo IAsyncResult, que será retornado pelo método BeginXXX.

Antes de visualizarmos os exemplos concretos desta funcionalidade, temos uma configuração a ser realizada na ConnectionString que devemos nos atentar para que as operações resultem. Nesta string de conexão com a base de dados, necessitamos definir um parâmetro que indicará ao .NET que esse banco de dados poderá trabalhar com processos assíncronos. Trata-se do parâmetro Asynchronous Processing que, se quisermos em algum momento da aplicação trabalhar de forma assíncrona, devemos definí-la como True. O código abaixo exemplifica como deverá ficar a string de conexão para suportar esta funcionalidade:

"integrated security=SSPI;data source=localhost;
    initial catalog=Northwind;Asynchronous Processing=True"

Já ara exemplificar toda a teoria até o momento, veremos os dois exemplos de operações assíncronas na base de dados, onde, em um dos casos, somente vamos lançar a operação assíncrona para ser realizada. Já no segundo exemplo, utilizaremos uma função de callback. É importante dizer que para simularmos uma operação demorada na base de dados SQL Server, utilizaremos o comando WAITFOR DELAY que, especificado um tempo, ele aguardará pelo mesmo até executar o procedimento que queremos que a base de dados faça. Nos exemplos abaixo estaremos passando para esse comando o valor de 10 segundos. Primeiramente analisaremos a forma de lançar um processo assíncrono na base de dados sem a utilização de callbacks:

using System.Data.SqlClient;
using System.Configuration;

namespace WindowsApplication2
{
    public partial class Form1 : Form
    {
        private SqlCommand _cmd;
        private SqlConnection _conn;
        private IAsyncResult _result;

        private void IniciaProcesso_Click(object sender, EventArgs e) {
            try {
                this._conn = 
                    new SqlConnection(GetConnStringFromConfigFile("ConnString"));
                this._cmd = 
                    new SqlCommand("WAITFOR DELAY '0:0:10'; " + 
                        "INSERT INTO Pedido (Cliente) VALUES ('Israel Aece')", this._conn);

                this._conn.Open();
                this._result = this._cmd.BeginExecuteNonQuery();
            }
            catch {
                this.TextBox1.Text = "Banco de dados indisponível.";
            }
        }

        private void VerificaProcesso_Click(object sender, EventArgs e) {
            try {
                if (!this._result.IsCompleted) {
                    this.TextBox1.Text = "Incompleto.";
                }
                else {
                    this.TextBox1.Text = "Finalizado. " +
                        this._cmd.EndExecuteNonQuery(this._result).ToString() +
                        " registro(s) afetado(s).";

                    this._conn.Close();
                }
            }
            catch (Exception ex) {
                this.TextBox1.Text = "Erro: " + ex.ToString();
                this._conn.Close();
            }
        }
    }
}

Ao clicar no botão “Iniciar Processo”, enviamos para a base de dados o comando a ser executado através do método BeginExecuteNonQuery do objeto SqlCommand que temos dentro do formulário. Depois de realizado isso, verificamos se o processo foi ou não finalizado através da propriedade IsCompleted do objeto IAsyncResult, a qual já vimos a sua funcionalidade um pouco mais acima. Se o processo não foi finalizado, definimos na caixa de texto a mensagem que ainda está em processamento. Do contrário, exibimos a quantidade de registros afetados pela query.

É importante dizer que até que o processo não for finalizado, a conexão com a base de dados não poderá ser fechada. Se notarmos, o método Close do objeto SqlConnection somente é chamado quando a propriedade IsCompleted retornar True, ou quando uma falha na execução da query for encontrada, pois o bloco Catch será disparado. A imagem abaixo ilustra esse processo que descrevemos acima:

Figura 1 – Verificando se o processo foi finalizado através da propriedade IsCompleted.

Callbacks

Ao contrário do exemplo anterior, com a utilização de callbacks temos a vantagem de executarmos algum código quando o processo assíncrono da base de dados for finalizado. Isso tira o encargo do usuário ficar verificando isso, ou seja, quando invocamos qualquer método BeginXXX, temos uma sobrecarga que aceitará um parâmetro do tipo AsyncCallback, que é um Delegate que apontará para um procedimento que será invocado automaticamente quando o processo finalizar. Iremos utilizar o mesmo cenário do exemplo anterior, só que agora com o auxílio dos callbacks. Vejamos o código já implementado:

using System.Data.SqlClient;
using System.Configuration;

namespace WindowsApplication2
{
    public partial class Form2 : Form
    {
        private SqlConnection _conn;

        private void IniciaProcesso_Click(object sender, EventArgs e) {
            try
            {
                this._conn = 
                    new SqlConnection(GetConnStringFromConfigFile("ConnString"));
                SqlCommand cmd =
                    new SqlCommand("WAITFOR DELAY '0:0:10'; " +
                        "INSERT INTO Pedido (Cliente) VALUES ('Israel Aece')", this._conn);

                this._conn.Open();
                AsyncCallback callback = new AsyncCallback(MeuCallback);
                cmd.BeginExecuteNonQuery(callback, cmd);
                this.TextBox1.Text = "Processando...";
            }
            catch {
                this.TextBox1.Text = "Banco de dados indisponível.";
            }
        }

        private void MeuCallback(IAsyncResult result) {
            SqlCommand cmd = (SqlCommand)result.AsyncState;
            this.TextBox1.Text = "Registros afetados: " +
                cmd.EndExecuteNonQuery(result).ToString();

            if (this._conn != null) this._conn.Close();
        }
    }
}

Analisando o código acima, criamos um objeto do tipo SqlConnection e, no evento Click do botão “IniciaProcesso”, atribuímos à instância ao mesmo, recuperando a ConnectionString do arquivo de configuração. Logo em seguida, criamos o objeto SqlCommand, o qual será responsável por criar o comando que será executado na base de dados e, por fim, criamos um delegate do tipo AsyncCallback, onde definimos o procedimento que será executado quando o processo na base de dados for finalizado. Para que o processo seja inicializado, invocamos o método BeginExecuteNonQuery, passando o delegate e o objeto que iremos utilizar no procedimento apontado pelo delegate. Neste caso, iremos mandar para o procedimento o próprio objeto SqlCommand, já que o utilizaremos para encerrar o processo.

Já no procedimento “MeuCallback”, o qual foi definido no delegate para ser processado quando o processo assíncrono for finalizado, reparem que, como parâmetro, recebemos um objeto do tipo IAsyncResult (isso é uma “exigência”, pois o delegate tem essa assinatura), e este por sua vez, fornece uma propriedade chamada AsyncState, a qual retorna um objeto definido pelo usuário, contendo informações sobre a operação assíncrona. Agora, simplesmente fazemos a conversão para o objeto SqlCommand e invocamos o método EndExecuteNonQuery para finalizar o processo assíncrono. É importante que até o retorno deste processo a conexão com a base de dados não pode ser fechada, ou seja, isso somente deverá ser feito quando o processo assíncrono for finalizado.

Figura 2 – Utilizando Callbacks.

Processo Assíncrono e ASP.NET

ASP.NET 2.0 já traz instrinsicamente uma infra-estrutura para trabalharmos com páginas/chamadas assíncronas, simplificando bastante a sua utilização. Isso pode ser usado em conjunto com esta nova funcionalidade de executar assincronamente queries na base de dados. É bastante comum em páginas ASP.NET acessarmos o conteúdo de uma determinada base de dados; sendo assim, podemos efetuar uma query assíncrona dentro de uma base de dados qualquer e retornar ao usuário o result-set e, conseqüentemente, popular um controle do tipo GridView. Devemos, neste caso, criar e codificar o evento PreRenderComplete, que será disparado imediatamente depois do processo assíncrono finalizado, mas antes da página ser renderizada, justamente para termos acesso ao controle GridView e aos demais itens da página. Veremos abaixo o código necessário para a execução de uma query assíncrona dentro de uma página também assíncrona:

using System.Data.SqlClient;

public partial class DB : System.Web.UI.Page
{
    private SqlConnection _conn;
    private SqlCommand _cmd;
    private SqlDataReader _reader;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            this.PreRenderComplete += new EventHandler(Page_PreRenderComplete);

            this.AddOnPreRenderCompleteAsync(
                new BeginEventHandler(IniciaProcesso),
                new EndEventHandler(FinalizaProcesso)
            );
        }
    }

    protected IAsyncResult IniciaProcesso(
        object sender, 
        EventArgs e, 
        AsyncCallback cb, 
        object state)
    {
        this._conn = new SqlConnection(GetConnStringFromConfigFile("ConnString"));
        this._conn.Open();
        this._cmd = new SqlCommand("SELECT * FROM Usuarios", this._conn);
        return this._cmd.BeginExecuteReader(cb, state);
    }

    protected void FinalizaProcesso(IAsyncResult ar)
    {
        this._reader = this._cmd.EndExecuteReader(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        if (this._reader.HasRows){
            this.GridView1.DataSource = _reader;
            this.GridView1.DataBind();		
        }
    }

    public override void Dispose()
    {
        if (this._conn != null) this._conn.Close();
        base.Dispose();
    }
}

Como as páginas assíncronas, suas funcionalidades e características fogem um pouco do escopo deste artigo, fica aqui uma referência para aqueles que querem entender um pouco mais sobre elas.

ADONET20.zip (118.08 kb)

ADO.NET – MARS

Multiple Active Result Sets (MARS) é uma das grandes novidades do ADO.NET 2.0. Essa funcionalidade permite múltiplos batches no banco de dados com apenas uma única conexão, algo que não era possível nas versões anteriores do ADO.NET.

Quando precisamos recuperar múltiplos result-sets no ADO.NET 1.x através de um objeto do tipo SqlDataReader, uma conexão exclusiva deve ser utilizada (anexada) para cada objeto SqlCommand. O MARS vem para evitar a criação de N conexões (geralmente com o mesmo servidor/base de dados) para recuperar múltiplos conjuntos de dados.

Vale lembrar que o MARS não permite a execução paralela de comandos, pois as queries são executadas de forma sequencial. Se você precisar da execução paralela em algum momento da sua aplicação, terá que considerar o uso de múltiplas conexões ao invés de utilizar MARS.

Por padrão, o MARS vem habilitado no SQL Server 2005, mas você pode configurá-lo caso não queira utilizá-lo. Para que possamos configurá-lo, definimos na string de conexão com a base de dados, marcando a propriedade MultipleActiveResultSets para True (ou False se quiser desabilitá-lo). O exemplo abaixo mostra como fica a string de conexão com o MARS habilitado:

"integrated security=SSPI;data source=localhost;
    initial catalog=Northwind;MultipleActiveResultSets=True"

Outro ponto importante do MARS é a questão do pool de conexões: habilitado o MARS ou não, a conexão é colocada em pool, mas elas não são consideradas iguais. Os pools de conexões são criados de acordo com a string de conexão; isso quer dizer que strings de conexões diferentes tem pools diferentes. Se abrimos duas conexões, uma delas com o MARS habilitado e outra não, quando elas forem devolvidas para o pool, cada uma delas ficará armazenada em um local diferente.

Para vermos o MARS em funcionamento, vamos analisar o seguinte cenário: temos duas tabelas na base de dados: Pedidos e Items do Pedido. Temos que exibir para o cliente, o pedido e os itens relacionados. O código abaixo exemplifica isso:

SqlConnection conn = null;
try
{
    conn = new SqlConnection(GetConnStringFromConfigFile("ConnString"));

    SqlCommand cmdPedidos = new SqlCommand("SELECT * FROM Pedido", conn);
    conn.Open();

    using (SqlDataReader drPedidos = cmdPedidos.ExecuteReader())
    {
        while (drPedidos.Read())
        {
            int pedidoID = drPedidos.GetInt32(0);
            string nome = drPedidos.GetString(1);
            Console.WriteLine(string.Format("Pedido: {0} - Cliente: {1}.", 
                pedidoID, nome));

            SqlCommand cmdItems = new 
                SqlCommand("SELECT * FROM Item WHERE PedidoID = @ID", conn);
            cmdItems.Parameters.AddWithValue("@ID", pedidoID);

            using (SqlDataReader drItems = cmdItems.ExecuteReader())
            {
                while (drItems.Read())
                {
                    string produto = drItems.GetString(1);
                    decimal valor = drItems.GetDecimal(2);
                    Console.WriteLine(
                        string.Format("    Produto: {0} - Valor: {1}", 
                            produto, valor));
                }
            }
        }
    }
}
catch
{
    //Log exception...
}
finally
{
    if (conn != null) conn.Close();
}

O método GetConnStringFromConfigFile retorna uma ConnectionString do arquivo de configuração dado uma key, a qual deverá conter a atributo MultipleActiveResultSets definido como True. Podemos reparar que, com uma única conexão, podemos recuperar dados de diversos DataReaders (drPedidos e drItems) sem a necessidade de termos um conexão específica servindo cada um deles. O resultado deste processamento, é na imagem mostrado abaixo:

Figura 1 – MARS em funcionamento.

ADONET20.zip (118.08 kb)

ADO.NET – Bulk Copy

O Microsoft SQL Server fornece um utilitário de linha de comando bastante popular, chamado de bcp. Basicamente, este utilitário tem a finalidadade de copiar grandes volumes de dados entre duas bases de dados SQL Server. Abaixo é mostrado a sua sintaxe:

bcp {[[database_name.][owner].]{table_name | view_name} | "query"}
    {in | out | queryout | format} data_file
    [-m max_errors] [-f format_file] [-e err_file]
    [-F first_row] [-L last_row] [-b batch_size]
    [-n] [-c] [-w] [-N] [-V (60 | 65 | 70)] [-6] 
    [-q] [-C code_page] [-t field_term] [-r row_term]
    [-i input_file] [-o output_file] [-a packet_size]
    [-S server_name[instance_name]] [-U login_id] [-P password]
    [-T] [-v] [-R] [-k] [-E] [-h "hint [,...n]"]

O ADO.NET 2.0, com estas várias inovações, incluiu também uma classe chamada SqlBulkCopy, que provê uma funcionalidade similar ao utilitário bcp, com a vantagem de ser agora gerenciado pelo .NET Framework e ter uma boa performance quanto ao utilitário de linha de comando. Mas a principal vantagem, é que essa “cópia em massa” de dados não precisa necessariamente ser entre base de dados SQL Server. Você pode ter os dados carregados em um objeto DataTable, um Array de objetos do tipo DataRow ou até mesmo de um DataReader e, com isso, você passa esses containers de dados para o objeto SqlBulkCopy, para que o mesmo possa fazer o seu trabalho, copiando os dados para a base de dados de destino. Para um exemplo bem simples, vamos analisar o código abaixo:

using System.Data.SqlClient;
//...
string connString = GetConnStringFromConfigFile("ConnString");

using (SqlConnection connSource = new SqlConnection(connString)) {
    SqlDataReader dr = null;
    try
    {
        SqlConnection connDest = new SqlConnection(connString);
        SqlCommand cmd = new SqlCommand("SELECT * FROM Tabela", connSource);
        connSource.Open();
        dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

        using (SqlBulkCopy copy = new SqlBulkCopy(connString)) {
            copy.DestinationTableName = "Tabela1";
            copy.WriteToServer(dr);
        }
    }
    finally
    {
        if (dr != null) dr.Close();
    }
}

Para exemplificar, criamos dois objetos do tipo SqlConnection, “connSource” e “connDest”, que são responsáveis pela conexão com a base de dados que será a fonte dos dados e a qual será o destino dos dados, respectivamente. Utilizamos um DataReader, para recuperarmos de forma rápida os dados e, em seguida, criamos o objeto SqlBulkCopy, informando em seu construtor a string de conexão com a base de dados de destino. O construtor deste objeto é sobrecarregado, podendo passar ao mesmo ao invés da string de conexão, um objeto SqlConnection da base de dados de destino, mas neste é obrigado que você controle a abertura e fechamento da conexão, o que não é necessário quando se passa a string de conexão, pois o processo que é manual na outra situação, aqui já estará encapsulado.

Através da propriedade DestinationTableName, informamos ao objeto SqlBulkCopy a tabela destino que receberá os dados. Quando as tabelas de origem e destino de dados são idênticas, ou seja, tem o mesmo número de colunas e também a mesma posição das colunas, o mapeamento entre as mesmas não é necessário. Entretanto, se a quantidade de colunas é diferente ou a ordem das colunas não são equivalentes, o mapeamento é necessário e, para isso, o objeto SqlBulkCopy fornece uma propriedade chamada ColumnMappings, a qual armazena uma coleção de itens do tipo SqlBulkCopyColumnMapping. Abaixo é mostrado um exemplo reduzido de como realizar esse mapeamento:

using (SqlBulkCopy copy = new SqlBulkCopy(connString)) {
    copy.DestinationTableName = "Tabela1";

    SqlBulkCopyColumnMapping colunaNome = new SqlBulkCopyColumnMapping("Nome", "NomeProduto")
    copy.ColumnMappings.Add(colunaNome);

    copy.WriteToServer(dr);
}

Além destas configurações, ainda temos três propriedades interessantes dentro do objeto SqlBulkCopy: BatchSize, BulkCopyTimeout e NotifyAfter. A primeira destas (BatchSize) é onde definimos através de um número inteiro, a quantidade de linhas que irá conter em cada batch (bloco). Se o valor informado para essa propriedade for 0 (zero), ela será executada em um simples batch. Já a propriedade BulkCopyTimeout é onde definimos, também através de um número inteiro, o número de segundos que a operação irá aguardar para ser completada, antes de dar timeout.

Por fim, temos ainda a propriedade NotifyAfter, onde definimos um número inteiro positivo que representará o número de linhas que serão processadas, antes de gerar um evento de notificação chamado SqlRowsCopied. Esta propriedade, em conjunto com o evento SqlRowsCopied (representado pelo delegate SqlRowsCopiedEventHandler), é utilizada geralmente para manipularmos objetos/controles da interface da aplicação, como por exemplo, exibir ao usuário um demostrativo do progresso da execução da cópia dos dados. Abaixo é mostrado um exemplo de como utilizá-la:

using (SqlBulkCopy copy = new SqlBulkCopy(connString))
{
    copy.DestinationTableName = "Tabela1";
    copy.NotifyAfter = 50;
    copy.SqlRowsCopied += new SqlRowsCopiedEventHandler(this.Notificacao);

    copy.WriteToServer(dr);
}

//...

private void Notificacao(object sender, SqlRowsCopiedEventArgs e)
{
    MessageBox.Show("Quantidade de Linhas copiadas: " +
        e.RowsCopied.ToString());
}

ADONET20.zip (118.08 kb)

ADO.NET – ConnectionStrings

Como já é de conhecimento da maioria dos desenvolvedores, ConnectionString é uma string que contém informações à respeito de uma determinada base de dados. É dentro dela que contém os dados necessários para se conectar ao servidor de banco de dados ou até mesmo à um arquivo de banco de dados. As principais informações que aqui temos são: servidor de banco de dados, caminho do arquivo de banco de dados, usuário e senha.

A ConnectionString é composta por uma lista de chave/valor, que são separadas por ponto e vírgula “;”. Abaixo temos alguns exemplos de ConnectionString, sendo:

  • Servidor de Banco de Dados – SQL Server
    • “Data Source=Aron1;Initial Catalog=pubs;User Id=sa;Password=123;”
  • Arquivo de Banco de Dados – Access
    • “Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:Base.mdb;User Id=admin;Password=123;”

Em ambos os exemplos, podemos reparar que as informações são sempre compostas da seguinte forma: key=valor;, independentemente de qual tipo de base de dados estamos utilizando. Mas vale lembrar que as configurações param por aí. Existem além dessas que vimos acima, uma série de outras opções que podemos definir dentro da ConnectionString, mas já foge um pouco do escopo desta seção/artigo.

Para constar, fica aqui uma referência para um site que contém as possíveis Strings de conexões com várias bases de dados existentes no mercado.

Devido a grande importância que tem as strings de conexões dentro de uma aplicação, a Microsoft decidiu incluir dentro do arquivo de configuração um elemento, chamado de connectionStrings, que permite a configuração destas, sem a necessidade de criarmos chaves dentro da seção appSettings (também do arquivo de configuração) para armazenarmos a conexão com a base de dados.

Dentro deste elemento, podemos definir várias strings de conexões que serão utilizadas no decorrer da aplicação. Para exemplificar isso, vejamos o código abaixo, onde fazemos a conexão com uma base de dados SQL Server local, referenciando o banco de dados NorthWind. A autenticação é efetuada através de Integrated Security. Em outras palavras quer dizer que a autenticação é efetuada com as credenciais do usuário que está logado no Windows:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add 
      name="ConnString" 
      connectionString=
         "integrated security=SSPI;data source=localhost;initial catalog=Northwind" />
  </connectionStrings>
</configuration>

Através do elemento add adicionamos as conexões desejadas. Seus principais atributos são:

Atributo Descrição
connectionString Armazena o valor da ConnectionString com uma determinada base de dados, sendo um atributo obrigatório, podendo conter um valor vazio, porém requerido.
name Define o nome da ConnectionString para ser recuperada no código da aplicação. É um atributo requerido.
providerName Contém o nome do provider para acessar os dados dessa base de dados. É um atributo requerido e, quando não informado, o padrão é o System.Data.SqlClient.

* Veremos mais a fundo em Código Genérico, ainda neste artigo.

Como já era esperado, a Microsoft criou também uma classe chamada XXXConnectionStringBuilder (onde XXX representa o provider que já está contido dentro do .NET Framework: Sql, OleDb, Oracle e Odbc), que fornece uma forma de criar e gerenciar as ConnectionStrings que serão utilizados para criar a conexão com a Base de Dados. Todas essas classes herdam de uma classe base, chamada DbConnectionStringBuilder, que está contida dentro do namespace System.Data.Common.

Estas classes tem a finalidade de fornecer ao desenvolvedor um construtor de strings de conexão fortemente tipado, onde teremos propriedades exclusivas para cada provider. Isso possibilitará definir os valores para cada uma dessas propriedades de acordo com a base de dados que iremos acessar. Vale lembrar que isso não impossibilita o desenvolvedor a criar uma ConnectionString inválida, já que os dados são avaliados somente em tempo de execução e, caso sejam incompatíveis, uma Exception será atirada ao cliente.

Para exemplificar, utilizaremos um objeto do tipo SqlConnectionStringBuilder fornecendo os mesmos dados de conexão do código que vimos um pouco mais acima:

using System.Data.SqlClient;
//...
SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder();
sb.DataSource = "localhost";
sb.InitialCatalog = "Northwind";
sb.IntegratedSecurity = true;
Console.WriteLine(sb.ConnectionString);

//Output:
//Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True

Note que o output é dado através da propriedade ConnectionString, que retorna um valor idêntico ao qual definimos o primeiro código. Quando utilizamos esta forma de criação de ConnectionStrings, não precisamos nos preocupar com as keys que serão embutidas dentro dela, já que o .NET se encarrega de trazer os valores corretos. Apenas temos neste momento que nos preocupar em enviar os dados certos para que a conexão com a base de dados seja efetuada com sucesso.

Mas ainda há como integrarmos a classe XXXConnectionStringBuilder e o que temos dentro do arquivo de configuração da aplicação. As classes XXXConnectionStringBuilder fornece um construtor parametrizado, onde recebe uma ConnectionString. Para podermos recuperar as informações do arquivo de configuração, o .NET Framework fornece dentro do namespace System.Configuration, classes que possibilitam isso. O código abaixo mostra uma função que retorna a ConnectionString do arquivo de configuração e atribui à classe XXXConnectionStringBuilder:

using System.Configuration;
using System.Data.SqlClient;
//...
private static string GetConnStringFromConfigFile(string key) {
    Configuration config =
        ConfigurationManager.OpenExeConfiguration(
        ConfigurationUserLevel.None);

    ConnectionStringsSection conStrings =
        config.ConnectionStrings as ConnectionStringsSection;

    if (config != null) {
        SqlConnectionStringBuilder s = new
            SqlConnectionStringBuilder(
                conStrings.ConnectionStrings[key].ConnectionString);

        return s.ConnectionString;
    }

    return string.Empty;
}
//...
Console.WriteLine(GetConnStringFromConfigFile("ConnString"));

Analisando o código acima, vemos que através do método OpenExeConfiguration abrimos o arquivo de configuração da aplicação. Esse método retorna um objeto do tipo Configuration, contendo todas as informações/seções do arquivo *.config em questão e esta classe, por sua vez, tem uma propriedade chamada ConnectionStrings que retorna um objeto do tipo ConnectionStringsSection, contendo as possíveis strings de conexão daquele arquivo.

Com a instância do objeto ConnectionStringsSection em mãos, podemos alcançar uma string de conexão específica (uma instância da classe ConnectionStringSettings), passando o name que definimos no arquivo de configuração. Finalmente criamos uma instância da classe SqlConnectionStringBuilder, e em seu construtor passamos a string de conexão do arquivo de configuração, através da propriedade ConnectionString.

O fato de termos isso agora dentro do objeto XXXConnectionStringBuilder nos proporciona uma certa flexibilidade, já que podemos alterar os valores (lembrando que as alterações não refletem no arquivo de configuração). Para exemplificar, depois de carregado a string de conexão do arquivo de configuração, vamos alterar o banco de dados de Nothwind para pubs:

//...
if (config != null) {
    SqlConnectionStringBuilder s = new
        SqlConnectionStringBuilder(
            conStrings.ConnectionStrings[key].ConnectionString);

    s.InitialCatalog = "pubs";
    return s.ConnectionString;
}
//...

Nota: Por padrão a DLL System.Configuration.dll não vem referenciada no projeto, pois se não a tivermos, classes de acesso às configurações ficarão indisponíveis. Para isso, clique com o botão direito do mouse em cima do projeto, Add Reference…, aba .NET e selecione System.Configuration.dll.

ADONET20.zip (118.08 kb)