Desenvolvimento - ADO.NET

Por dentro do ADO.NET 2.0

Novas funcionalidades foram adicionadas no .NET Framework e o ADO.NET não poderia ter ficado sem estas novas "features". Chamado de ADO.NET 2.0, ele traz intrinsicamente muitas coisas que eram impossíveis de fazer nas versões anteriores ou exigia muito código até alcançar o resultado desejado.

por Israel Aéce



Novas funcionalidades foram adicionadas no .NET Framework e o ADO.NET não poderia ter ficado sem estas novas "features". Chamado de ADO.NET 2.0, ele traz intrinsicamente muitas coisas que eram impossíveis de fazer nas versões anteriores ou exigia muito código até alcançar o resultado desejado.

As melhorias são imensas, principalmente no que diz respeito a criação código Genérico para múltiplos bancos de dados. A Microsoft também se preocupou em dar toda a infraestrutura na realização de trabalhos assíncronos na base de dados (podendo até mesmo trabalhar em conjunto com as páginas assíncronas do ASP.NET 2.0), entre outras novidades bem interessantes.

Este artigo tem a finalidade de apresentar essas novas funcionalidades, onde as mesmas estarão disponibilizadas em cada tópico abaixo:

Conteúdo

  • ConnectionStrings
    Uma String de conexão é utilizada para acessar um determinado banco de dados. É dentro desta que definimos o servidor (ou arquivo de banco de dados), usuário, senha, entre outros dados que são separados por ponto e vírgula ";". Dentro do arquivo de configuração da aplicação, temos um novo elemento chamado connectionStrings, o qual tem a finalidade de armazenar os dados de conexão com a base de dados e, além deste, temos classes que dão todo o suporte para manipular estas configurações via código.

  • Trabalho Assíncrono
    O provider SqlClient nos fornece opções para executarmos queries e comandos dentro de uma base de dados SQL Server de forma assíncrona. Isso é possível graças a adição de métodos como BeginXXX e EndXXX para cada uma das opções: ExecuteReader, ExecuteXmlReader e ExecuteNonQuery, as quais veremos detalhadamente nesta seção.

  • MARS
    Multiple Active Result Sets - Isso possibilita o que muitos desenvolvedores tentam fazer no ADO.NET 1.x (talvez ainda tentem, já que era possível nas versões anteriores (ADO)), ou seja, executar múltiplos comandos com uma única conexão, algo que não era possível nas versões 1.x do ADO.NET.

  • Bulk Copy
    O ADO.NET agora fornece uma classe que tem a mesma finalidade do utilitário bcp, ou seja, copiar de forma rápida um grande volume de dados para tabelas ou views dentro do SQL Server. A vantagem é que não é necessário ser de SQL Server para SQL Server, ou seja, a fonte de dados pode ser um DataReader ou mesmo um DataTable.

  • Escrevendo Código Genérico
    Uma das grandes novidades do ADO.NET 2.0 é o suporte interno à criação de código genérico para múltiplas base de dados. Isso evita de implementarmos o padrão Abstract Factory ou utilizarmos as Interfaces genéricas. Veremos como implementar nesta seção as Provider Factories e a escrita de código genérico de acesso à dados.

Download do Código Fonte

Conclusão: Ao longo desta série de artigos vimos detalhadamente algumas das principais funcionalidades que agora existem dentro do ADO.NET 2.0, podendo ser consumido dentro de qualquer projeto .NET. Ainda existem outras novidades que não foram aqui detalhadas, tais como as novas funcionalidades do DataSet, Nullables Types e SqlCacheDependency, que precisaria de um artigo somente para isso. De qualquer forma, o artigo mostrou as principais mudanças que serão fundamentais na construção de uma estrutura de acesso aos dados.

function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i 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>
*.config

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
Imports System.Data.SqlClient
"...
Dim sb As 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
C# VB.NET

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"));
Imports System.Configuration
Imports System.Data.SqlClient
"...
Private Shared Function GetConnStringFromConfigFile(ByVal key As String) As String
    Dim config As Configuration = _
        ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None)

    Dim connStrings As ConnectionStringsSection = config.ConnectionStrings

    If Not IsNothing(connStrings) Then
        Dim sb As New SqlConnectionStringBuilder( _
            connStrings.ConnectionStrings(key).ConnectionString)

        Return sb.ConnectionString
    End If

    Return String.Empty
End Function
"...
Console.WriteLine(GetConnStringFromConfigFile("ConnString"))
C# VB.NET

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;
}
//...
"...
If Not IsNothing(connStrings) Then
    Dim sb As New SqlConnectionStringBuilder( _
        connStrings.ConnectionStrings(key).ConnectionString)

    s.InitialCatalog = "pubs"
    Return sb.ConnectionString
End If
"...
C# VB.NET

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. function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i 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"
String de Conexão

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();
            }
        }
    }
}
Imports System.Data.SqlClient
Imports System.Configuration

Public Class Form1

    Private _cmd As SqlCommand
    Private _conn As SqlConnection
    Private _result As IAsyncResult

    Private Sub IniciaProcesso_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles IniciaProcesso.Click

        Try
            Me._conn = New SqlConnection(GetConnStringFromConfigFile("connString"))
            Me._cmd = New SqlCommand("WAITFOR DELAY "0:0:10"; " & _
                "INSERT INTO Pedido (Cliente) VALUES ("Israel Aece")", Me._conn)

            Me._conn.Open()
            Me._result = Me._cmd.BeginExecuteNonQuery()
        Catch
            Me.TextBox1.Text = "Banco de dados indisponível."
        End Try
    End Sub

    Private Sub VerificaProcesso_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles VerificaProcesso.Click

        Try
            If Not _result.IsCompleted Then
                Me.TextBox1.Text = "Incompleto."
            Else
                Me.TextBox1.Text = "Finalizado. " & _
                    Me._cmd.EndExecuteNonQuery(Me._result).ToString() & _
                    " registro(s) afetado(s)."

                Me._conn.Close()
            End If
        Catch ex As Exception
            Me.TextBox1.Text = "Erro: " & ex.ToString()
            Me._conn.Close()
        End Try
    End Sub

End Class
C# VB.NET

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();
        }
    }
}
Imports System.Data.SqlClient
Imports System.Configuration

Public Class Form2

    Private _conn As SqlConnection

    Private Sub IniciaProcesso_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles IniciaProcesso.Click
        Try
            Me._conn = New SqlConnection(GetConnStringFromConfigFile("ConnString"))
            Dim cmd As New _
                SqlCommand("WAITFOR DELAY "0:0:10"; " & _
                    "INSERT INTO Pedido (Cliente) VALUES ("Israel Aece")", _conn)

            _conn.Open()
            Dim callback As New AsyncCallback(AddressOf MeuCallback)
            cmd.BeginExecuteNonQuery(callback, cmd)
            Me.TextBox1.Text = "Processando..."
        Catch ex As Exception
            Me.TextBox1.Text = "Banco de dados indisponível."
        End Try
    End Sub

    Private Sub MeuCallback(ByVal result As IAsyncResult)
        Dim command As SqlCommand = DirectCast(result.AsyncState, SqlCommand)
        Me.TextBox1.Text = "Registros afetados: " & _
            command.EndExecuteNonQuery(result).ToString()

        If Not IsNothing(_conn) Then _conn.Close()
    End Sub

End Class
C# VB.NET

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();
    }
}
Imports System.Data.SqlClient

Partial Class DB

    Inherits System.Web.UI.Page

    Private _conn As SqlConnection
    Private _cmd As SqlCommand
    Private _reader As SqlDataReader

    Protected Sub Page_Load( _
        ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load

        If Not Page.IsPostBack Then
            Me.AddOnPreRenderCompleteAsync( _
                New BeginEventHandler(AddressOf IniciaProcesso), _
                New EndEventHandler(AddressOf FinalizaProcesso) _
            )
        End If
    End Sub

    Protected Function IniciaProcesso( _
        ByVal sender As Object, _
        ByVal e As EventArgs, _
        ByVal cb As AsyncCallback, _
        ByVal state As Object) As IAsyncResult

        Me._conn = New SqlConnection(GetConnStringFromConfigFile("ConnString"))
        Me._conn.Open()
        Me._cmd = New SqlCommand("SELECT * FROM Usuarios", Me._conn)
        Return Me._cmd.BeginExecuteReader(cb, state)
    End Function

    Protected Sub FinalizaProcesso(ByVal ar As IAsyncResult)
        Me._reader = Me._cmd.EndExecuteReader(ar)
    End Sub

    Protected Sub Page_PreRenderComplete( _
        ByVal sender As Object, _
        ByVal e As EventArgs) Handles Me.PreRenderComplete

        If Me._reader.HasRows Then
            Me.GridView1.DataSource = Me._reader
            Me.GridView1.DataBind()
        End If
    End Sub

    Public Overrides Sub Dispose()
        If Not IsNothing(Me._conn) Then Me._conn.Close()
        MyBase.Dispose()
    End Sub

End Class
C# VB.NET

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.

function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i 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"
String de Conexão

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();
}
Dim conn As SqlConnection = Nothing
Try
    conn = New SqlConnection(GetConnStringFromConfigFile("ConnString"))

    Dim cmdPedidos As New SqlCommand("SELECT * FROM Pedido", conn)
    conn.Open()

    Using drPedidos As SqlDataReader = cmdPedidos.ExecuteReader
        While drPedidos.Read()
            Dim pedidoID As Integer = drPedidos.GetInt32(0)
            Dim nome As String = drPedidos.GetString(1)
            Console.WriteLine(String.Format("Pedido: {0} - Cliente: {1}.", _
                    pedidoID, nome))

            Dim cmdItems As New _
                SqlCommand("SELECT * FROM Item WHERE PedidoID = @ID", conn)
            cmdItems.Parameters.AddWithValue("@ID", pedidoID)

            Using drItems As SqlDataReader = cmdItems.ExecuteReader()
                While drItems.Read()
                    Dim produto As String = drItems.GetString(1)
                    Dim valor As Decimal = drItems.GetDecimal(2)
                    Console.WriteLine( _
                            String.Format("    Produto: {0} - Valor: {1}", _
				produto, valor))
                End While
            End Using
        End While
    End Using
Catch ex As Exception
    "log exception
Finally
    If Not IsNothing(conn) Then conn.Close()
End Try
C# VB.NET

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.
function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i 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]"]
bcp

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();
    }
}
Imports System.Data.SqlClient
"...
Dim connString As String = GetConnStringFromConfigFile("ConnString")

Using connSource As New SqlConnection(connString)
    Dim dr As SqlDataReader = Nothing
    Try
        Dim connDest As SqlConnection = New SqlConnection(connString)
        Dim cmd As New SqlCommand("SELECT * FROM Tabela", connSource)
        connSource.Open()
        dr = cmd.ExecuteReader(CommandBehavior.CloseConnection)

        Using copy As New SqlBulkCopy(connString)
            copy.DestinationTableName = "Tabela1"
            copy.WriteToServer(dr)
        End Using
    Finally
        If Not IsNothing(dr) Then dr.Close()
    End Try
End Using
C# VB.NET

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);
}
Using copy As New SqlBulkCopy(connString)
    copy.DestinationTableName = "Tabela1"

    Dim colunaNome As New SqlBulkCopyColumnMapping("Nome", "NomeProduto")
    copy.ColumnMappings.Add(colunaNome)

    copy.WriteToServer(dr)
End Using
C# VB.NET

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());
}
Using copy As New SqlBulkCopy(connString)
    copy.DestinationTableName = "Tabela1"
    copy.NotifyAfter = 50
    AddHandler copy.SqlRowsCopied, AddressOf Me.Notificacao

    copy.WriteToServer(dr)
End Using

"...

Private Sub Notificacao(ByVal sender As Object, _
    ByVal e As SqlRowsCopiedEventArgs)

    MessageBox.Show("Quantidade de Linhas copiadas: " & _
        e.RowsCopied.ToString())
End Sub
C# VB.NET
function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Escrevendo Código Genérico

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;
}
Public Function GetConnection() As IDbConnection
    Dim provider As String = GetProviderFromConfig()
    Dim conn As IDbConnection

    Select Case provider
        Case "SQLServer"
            conn = New SqlConnection()
        Case "Oracle"
            conn = New OracleConnection()
    End Select

    Return conn
End Function
C# VB.NET

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;
}
Public Function GetConnection() As DbConnection
    Dim provider As String = GetProviderFromConfig()
    Dim conn As DbConnection

    Select Case provider
        Case "SQLServer"
            conn = New SqlConnection()
        Case "Oracle"
            conn = New OracleConnection()
    End Select

    Return conn
End Function
C# VB.NET

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>
*.config

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 

		
Private Sub btnListar_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnListar.Click

    Dim connStrings As ConnectionStringSettingsCollection = _
        ConfigurationManager.ConnectionStrings

    For i As Integer = 1 To connStrings.Count - 1
        Dim cs As ConnectionStringSettings = connStrings.Item(i)
        Dim factory As DbProviderFactory = _
            DbProviderFactories.GetFactory(cs.ProviderName)

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

        Dim reader As DbDataReader = Nothing
        Try
            conn.Open()
            reader = cmd.ExecuteReader(CommandBehavior.CloseConnection)

            If reader.HasRows Then
                While reader.Read()
                    Me.lstClientes.Items.Add(reader.GetString(1))
                End While
            Else
                MessageBox.Show("Não há registros.")
            End If
        Catch
            MessageBox.Show("Ocorreu um erro.")
        Finally
            If Not IsNothing(reader) Then reader.Close()
        End Try
    Next
End Sub
C# VB.NET

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.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.