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éceAs 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.
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:
|
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:
|
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. |
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 |
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:
|
* 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:
|
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; iPrivate 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.