msdn11_capa.jpg

Clique aqui para ler todos os artigos desta edição

 

Migrando do ADO para o ADO.NET, Parte 2

por John Papa

 

À medida que progredir no uso do ADO.NET, você precisará saber como abordar no ADO.NET situações com as quais costumava lidar no ADO. Assim como as soluções n-tiered desenvolvidas por meio do Visual Basic®, C++ e ASP geralmente dependem do ADO para suas necessidades de acesso aos dados, os Windows® Forms, os Web Forms e os Web services dependem do ADO.NET. Na coluna do mês passado (edição 10 da MSDN Magazine Brasil), analisamos como tratar diversos cenários de acesso a dados usando o ADO.NET a partir da perspectiva de desenvolvimento com o ADO tradicional. Alguns desses tópicos incluem a persistência de rowsets para XML, lidar com cursores firehose forward-only e as várias maneiras de executar objetos Command.

Na coluna deste mês, continuarei a analisar as situações de desenvolvimento por meio do ADO.NET e descreverei como tratá-las com as técnicas ADO tradicionais. Começarei analisando as situações em que os cursores forward-only, estáticos, keyset e dinâmicos do ADO clássico foram comumente usados. Analisarei também como as questões de concorrência são tratadas e como os rowsets desconectados evoluíram do ADO para ADO.NET. Em seguida, demonstrarei como usar o código que trata atualizações de batch no ADO tradicional e convertê-lo para que funcione com o ADO.NET, usando o DataAdapter e seus quatro objetos Command.

 

Divergência de Recordset

A maioria dos recursos de Recordset do ADO é dividida em três objetos-chave no ADO.NET: o DataReader, o DataSet e o DataAdapter (consulte a Figura 1).

 

image001.gif

Figura 1 O Recordset ADO

 

O objeto ADO.NET DataReader é projetado para ser um cursor no lado do servidor (server-side), forward-only e de somente-leitura. O objeto ADO.NET DataSet consiste em uma ferramenta de armazenamento desconectada para rowsets. Ele armazena registros sem precisar manter uma conexão com uma fonte de dados e, na verdade, ele não se importa com a fonte de dados da qual os rowsets são derivados. O DataSet é um objeto binário quando armazenado na memória, mas ele também pode ser facilmente serializado para/de XML. Ele é semelhante ao objeto ADO Recordset, com seu CursorType definido como adOpenStatic, e CursorLocation definido como adUseClient, quando ele tiver sido desconectado de seu objeto Connection associado (por meio da propriedade ActiveConnection do Recordset). O objeto ADO.NET DataAdapter consiste em uma ponte entre a conexão e os objetos DataSet. Ele pode carregar um DataSet a partir de uma fonte de dados por meio de uma conexão e pode atualizar a fonte de dados com as alterações armazenadas em um DataSet. O comportamento do ADO Recordset depende de suas configurações de propriedade (incluindo as propriedades CursorType e CursorLocation). No ADO.NET, são criados objetos separados para lidar com essas situações específicas, em vez de se utilizar um objeto para atender a todos os cenários.

 

Cursores Firehose

O ADO tradicional expõe quatro tipos diferentes de cursores que alteram a forma como o objeto ADO Recordset funciona. No ADO Recordset, o objeto se comporta de forma muito diferente, dependendo de como sua propriedade CursorType é definida. Por exemplo, definindo-se CursorType como adOpenForwardOnly, o Recordset permanece conectado à sua fonte de dados e precisa ser percorrido em uma direção contínua (forward-only). No entanto, quando você define a propriedade CursorType como adOpenDynamic, o Recordset pode ser percorrido para frente ou para trás, ou você pode até fazer com que o cursor salte para uma linha específica. Por intermédio de suas propriedades CursorType e CursorLocation, o objeto ADO Recordset utiliza a abordagem de reunir várias soluções em um único objeto. O ADO.NET usa um comportamento diferente, já que possui diferentes objetos e métodos para lidar com situações específicas.

No ADO clássico, os cursores forward-only e somente-leitura são implementados definindo-se CursorType como adOpenForwardOnly e CursorLocation como adUseServer (que são também as definições padrão). Isso faz com que o objeto Recordset assuma a forma de um cursor forward-only no lado do servidor. O método MoveNext reposiciona o Recordset na próxima linha e o método MovePrevious não é aceito em nenhuma circunstância. No entanto, você poderá chamar o método MoveFirst em um Recordset. Entretanto, esse método é um tanto enganoso, já que ele não se reposiciona no início do rowset atual. Em vez disso, ele chama a instrução SQL original e preenche novamente o Recordset a partir do zero, movendo-se assim para o próximo registro.

Você pode constatar isso facilmente abrindo a ferramenta SQL Profiler e observando a execução do SQL a cada vez que o método MoveFirst é executado em um ADO Recordset tradicional com um CursorType definido como adOpenForwardOnly. Esse tipo de cursor é geralmente usado em aplicações onde é necessário percorrer milhares de linhas (ou mais) individualmente (uma por vez) ou quando é necessário um rowset menor mas que só precise ser percorrido uma vez, possivelmente para ser carregado em uma lista de seleção:

 

'—Cursor Firehose Forward-only no ASP e no ADO

Set oRs.ActiveConnection = oCn

oRs.LockType = adLockReadOnly

oRs.CursorType = adOpenForwardOnly

oRs.CursorLocation = adUseServer

oRs.Open sSQL

 

O equivalente mais próximo desse tipo de cursor firehose do ADO tradicional é o objeto ADO.NET DataReader. Assim como o Recordset ADO forward-only tradicional, o DataReader permanece conectado à fonte de dados enquanto é aberto e só pode ser percorrido na direção à frente. Entretanto, existem diferenças. Uma delas é que os tipos DataReader são escritos especificamente para um provedor de dados como o SQL Server™ (a classe SqlDataReader) ou para uma fonte de dados ODBC (a classe OdbcDataReader). O objeto DataReader do ADO.NET é extremamente eficiente, já que foi criado especificamente com a finalidade de implementar cursores forward-only de somente-leitura. O cursor forward-only do ADO clássico é implementado por meio do mesmo objeto Recordset como um Recordset desconectado ou até mesmo como um Recordset sensível à fonte de dados. O DataReader é projetado exclusivamente para ser um cursor firehose simples.

 

//-- ASP.NET e ADO.NET em C#

SqlDataReader oDr = oCmd.ExecuteReader();

 

Cursores Sensíveis a Dados

Os cursores forward-only do ADO tradicional são agora tratados pelo objeto DataReader. E o que aconteceu com os cursores no lado do servidor keyset e dinâmicos (CursorType = adOpenKeySet and CursorType = adOpenDynamic) que são sensíveis a alterações no banco de dados subjacente? As versões atuais do ADO.NET não expõem um cursor multidirecional, rolável e atualizável no lado do servidor. No entanto, o ADO.NET oferece maneiras de contornar esses tipos de cursores no lado do servidor, já que existem outras técnicas recomendadas que você deve considerar.

Além do uso do DataSet combinado ao DataAdapter para recuperar e atualizar dados, existem algumas boas alternativas ao uso de cursores no lado do servidor que possam ser rolados no cliente. Por exemplo, você poderia usar stored procedures, que são muito eficientes na execução de processamento SQL no lado do servidor. Você também poderia recuperar os dados de um cursor forward-only no lado do servidor por meio de um DataReader e, em seguida, fazer atualizações separadas com a ajuda de objetos Command. No entanto, geralmente existem maneiras mais eficazes de fazer modificações nos dados do que usar cursores no lado do servidor atualizáveis.

Um dos problemas dos cursores roláveis no lado do servidor em um ambiente n-tiered é que eles requerem que o estado seja mantido no servidor. Desse modo, se um aplicativo usou um cursor rolável no lado do servidor na camada intermediária, a camada do cliente precisará manter uma conexão com a camada de negócios na qual reside esse cursor. Desse modo, os objetos de negócio persistem na tela do aplicativo cliente que também use um cursor rolável. Essas questões de escalabilidade estão bem documentadas, mas, é claro, existem situações em que os cursores no lado do servidor são úteis, por exemplo, quando houver necessidade de um cursor firehose forward-only. Por exemplo, os cursores no lado do servidor foram colocados em funcionamento quando um aplicativo quis ser posto a par dos problemas de concorrência de dados.

No ADO tradicional, era possível abrir um Recordset com um cursor dinâmico que permitia que o Recordset visse todas as inserções, alterações e exclusões feitas por outros usuários. Esse tipo de cursor no lado do servidor é sensível às alterações feitas por outros usuários e poderá ser usado em aplicativos para assumir uma abordagem preventiva quando surgirem problemas de concorrência. Por exemplo, os cursores dinâmicos no ADO tradicional eram freqüentemente usados quando um aplicativo estava prestes a salvar alterações no banco de dados mas queria certificar-se de ser notificado quando alguém fizesse alteração no mesmo registro primeiro. Quando um usuário alterava o valor de um registro contido em um Recordset dinâmico, o valor era automaticamente atualizado no Recordset dinâmico no lado do servidor:

 

'—Cursor Dinâmico no ASP e no ADO

Set oRs.ActiveConnection = oCn

oRs.LockType = adLockOptimistic

oRs.CursorType = adOpenDynamic

oRs.CursorLocation = adUseServer

oRs.Open sSQL

 

Concorrência e Cursores no Lado do Servidor

Os problemas de concorrência são relevantes e comuns em muitos aplicativos empresariais; no entanto, o custo de usar cursores no lado do servidor para solucionar isso pode sair muito alto. Então, quais alternativas o ADO.NET oferece para lidar com os problemas de concorrência? Uma técnica comum é permitir que o aplicativo envie as alterações ao banco de dados e fazer com que ele gere um tipo especial de exceção sempre que vir um problema de concorrência.

Existe um tipo especial de exceção denominado DBConcurrencyException que deriva da classe Exception básica. Como o DataSet armazena os valores propostos e originais de cada coluna em uma linha, ele sabe como compará-los em relação ao banco de dados para detectar automaticamente problemas de concorrência. Por exemplo, suponha que um usuário altere o valor do campo firstname de Lloyd para Lamar e envie as alterações para o banco de dados. As alterações, que são armazenadas em um DataSet, são passadas ao banco de dados por meio do método Update do DataAdapter. Antes de os dados serem salvos, se o nome do banco de dados não for mais Lloyd (mas sim Lorenzo), será gerada uma exceção DBConcurrencyException. Essa exceção pode ser capturada e tratada adequadamente das maneiras mais apropriadas às necessidades do aplicativo (você poderia, por exemplo, impor uma regra (o ultimo é o primeiro), dando ao usuário a opção de cancelar as alterações, substituir ou forçar a atualização de qualquer maneira (ou qualquer outra técnica).

No código da Listagem 1, você notará um método de exemplo que aceite um DataSet e envie as alterações no DataSet para o banco de dados por meio do método Update de um DataAdapter. Se for detectado um problema de concorrência, a exceção DBConcurrencyException será gerada e capturada pelo bloco catch específico. Nesse ponto, a transação poderia ser restabelecida e a exceção poderia ser novamente gerada até ser finalmente capturada pelo aplicativo cliente. Assim, o usuário poderia ser notificado e questionado sobre a maneira de lidar com ela. Além disso, analise cuidadosamente a ordem dos blocos catch. Por exemplo, se o bloco catch genérico aparecesse primeiro, então ele teria capturado o problema da concorrência. Uma chave para o tratamento de exceções é certificar-se de que os blocos catch das exceções mais específicas apareçam antes dos outros blocos catch, a fim de que eles sejam capturados primeiro.

 

Listagem 1 Capturando Problemas de Concorrência no ADO.NET

public DataSet SaveData(DataSet oDs)

{

  string sMethodName = "[public void SaveData(DataSet oDs)]";

 

  //================================================

  //-- Estabeleça variáveis locais

  //=================================================

  string sProcName;

  string sConnString = "Server=(local);

                        Database=Northwind;

                        Integrated Security=SSPI";

  SqlDataAdapter oDa = new SqlDataAdapter();

  SqlTransaction oTrn = null;

  SqlConnection oCn = null;

  SqlCommand oInsCmd = null;

  SqlCommand oUpdCmd = null;

  SqlCommand oDelCmd = null;

 

  try

  {

   //===============================================

   //-- Configure a Conexão

   oCn = new SqlConnection(sConnString);

 

   //===============================================

   //-- Abra a Conexão e crie a Transação

   oCn.Open();

   oTrn = oCn.BeginTransaction();

   //===============================================

   //-- Configure o Comando INSERT

   sProcName = "prInsert_Order";

   oInsCmd = new SqlCommand(sProcName, oCn, oTrn);

   oInsCmd.CommandType = CommandType.StoredProcedure;

   oInsCmd.Parameters.Add(new

           SqlParameter("@sCustomerID",

           SqlDbType.NChar, 5, "CustomerID"));

   oInsCmd.Parameters.Add(new

           SqlParameter("@dtOrderDate",

           SqlDbType.DateTime, 8,"OrderDate"));

   oInsCmd.Parameters.Add(new

           SqlParameter("@sShipCity",

           SqlDbType.NVarChar, 30, "ShipCity"));

   oInsCmd.Parameters.Add(new

           SqlParameter("@sShipCountry",

           SqlDbType.NVarChar, 30, "ShipCountry"));

   oDa.InsertCommand = oInsCmd;

 

   //==============================================

   //-- Configure o Comando UPDATE

   sProcName = "prUpdate_Order";

   oUpdCmd = new SqlCommand(sProcName, oCn, oTrn);

   oUpdCmd.CommandType = CommandType.StoredProcedure;

   oUpdCmd.Parameters.Add(new

           SqlParameter("@nOrderID",

           SqlDbType.Int, 4, "OrderID"));

   oUpdCmd.Parameters.Add(new

           SqlParameter("@dtOrderDate",

           SqlDbType.DateTime, 8,"OrderDate"));

   oUpdCmd.Parameters.Add(new

           SqlParameter("@sShipCity",

           SqlDbType.NVarChar, 30, "ShipCity"));

   oUpdCmd.Parameters.Add(new

           SqlParameter("@sShipCountry",

           SqlDbType.NVarChar, 30, "ShipCountry"));

   oDa.UpdateCommand = oUpdCmd;

 

   //===============================================

   //-- Configure o Comando DELETE

   sProcName = "prDelete_Order";

   oDelCmd = new SqlCommand(sProcName, oCn, oTrn);

   oDelCmd.CommandType = CommandType.StoredProcedure;

   oDelCmd.Parameters.Add(new

           SqlParameter("@nOrderID",

           SqlDbType.Int, 4, "OrderID"));

   oDa.DeleteCommand = oDelCmd;

 

   //===============================================

   //-- Salve todas as alterações no banco de dados

   oDa.Update(oDs.Tables["Orders"]);

   oTrn.Commit();

   oCn.Close();

  }

  catch (DBConcurrencyException exDBConcurrency)

  {

    //==============================================

    //-- Restabeleça (roll back) a transação

    //==============================================

    oTrn.Rollback();

 

    //--------------------------------------------

    //Pode querer gerar novamente Exceção nesse ponto

    //Isso depende de como você trata o problema

    //da concorrência

    //--------------------------------------------

    //-- throw(exDBConcurrency);

  }

  catch (Exception ex)

  {

    //==============================================

    //-- Restabeleça (roll back) a transação

    //==============================================

    oTrn.Rollback();

 

    //----------------------------------------------

    // Gere novamente a Exceção

    //----------------------------------------------

  throw;

  }

  finally

  {

     oInsCmd.Dispose();

     oUpdCmd.Dispose();

     oDelCmd.Dispose();

     oDa.Dispose();

     oTrn.Dispose();

     oCn.Dispose();

  }

  oCn.Close();

  return oDs;

}

 

Atualizações em Lote

Uma situação em que o objeto ADO Recordset costuma ser usado é para atualizações de lote. Por exemplo, em um aplicativo n-tier, um Recordset ADO poderia recuperar um rowset, desconectar-se da fonte de dados e enviar o Recordset para a camada do cliente. No aplicativo cliente, as alterações poderiam ser feitas em várias linhas do Recordset ADO desconectado. Em seguida, o Recordset poderia ser enviado de volta à camada intermediária, reconectado ao banco de dados por meio de um objeto Connection e ter suas alterações aplicadas ao banco de dados por meio do método UpdateBatch. O método UpdateBatch pega todas as linhas alteradas no Recordset e aplica as alterações na tabela de origem que é indicada na consulta da fonte do Recordset.

O ADO.NET também oferece recursos predefinidos para tratar atualizações em lote. O DataSet é uma coleção de rowsets independentes, sempre desconectados, que armazenam tanto os valores originais quanto atuais de suas linhas e colunas de rowsets. O DataSet pode ser enviado a um aplicativo cliente para que sejam feitas alterações nele. Em seguida, ele pode ser enviado à camada intermediária, onde as alterações feitas podem ser aplicadas ao banco de dados por meio de um objeto DataAdapter.

A Listagem 1 mostra como o ADO.NET DataAdapter pode usar seus vários objetos Command para representar os comandos SQL SELECT, INSERT, UPDATE e DELETE:

 

//-- Definindo os objetos Command do ADO.Net DataAdapter

oDa.InsertCommand = oInsCmd;

oDa.UpdateCommand = oUpdCmd;

oDa.DeleteCommand = oDelCmd;

oDa.Update(oDs.Tables["Orders"]);

 

A técnica ADO.NET proporciona mais flexibilidade do que a técnica de atualização em lote do ADO tradicional no que se refere à forma de aplicar as atualizações no banco de dados. O ADO tradicional decide onde salvar as alterações examinando a propriedade Source do Recordset ADO. Por exemplo, suponha que o código-fonte do Recordset seja:

 

SELECT OrderID, CustomerID, OrderDate, ShipCity, ShipCountry FROM Orders

 

Nesse caso, depois que as alterações do lote tiverem sido feitas no Recordset e o método UpdateBatch do Recordset tiver sido chamado, o Recordset precisará inferir para onde enviar as alterações. Ele examina a instrução SQL do código-fonte e determina que a chave primária (OrderID) da tabela Orders está na instrução e que há apenas uma tabela em uso. Desse modo, ele pode usar a instrução SQL para criar instruções UPDATE, INSERT e DELETE para acessar a tabela Orders. Para que o Recordset derive as consultas de ação, ele precisa conhecer o identificador de linha exclusivo. Além disso, as stored procedures não podem ser usados com essa técnica para atualizar o banco de dados, já que as instruções são criadas implicitamente. O seguinte código 2.x do ADO mostra como o Recordset que contém Orders poderia ser atualizado em um aplicativo cliente:

 

'-- Usando ADO tradicional,

'—Atualizando duas linhas na camada do cliente

oRs.Find "CustomerID = 'VINET'"

oRs.Fields("ShipCity") = "Somewhere"

oRs.Update

oRs.Find "CustomerID = 'CHOPS'"

oRs.Fields("ShipCity") = "Elsewhere"

oRs.Update

 

Em seguida, depois que o Recordset é retornado à camada intermediária, o seguinte fragmento de código poderá ser chamado para aplicar as alterações feitas no banco de dados:

 

'-- Usando ADO tradicional,

'—Enviando as duas linhas atualizadas para o banco de dados, na camada de negócios

oRs.UpdateBatch

 

O ADO.NET CommandBuilder funciona de forma similar, já que ele pode gerar automaticamente os objetos UpdateCommand, InsertCommand e DeleteCommand para o DataAdapter. O processo de gerar automaticamente os comandos tem um overhead. É mais eficiente especificar explicitamente as instruções INSERT, UPDATE e DELETE que você quer usar. O uso do CommandBuilder significa um desempenho mais lento e menos controle sobre como as alterações são aplicadas a uma fonte de dados se comparado a indicar explicitamente os comandos.

As instruções SELECT, INSERT, UPDATE e DELETE exatas são geralmente conhecidas na fase de design e podem ser usadas para preencher as quatro propriedades Comamnd individuais do ADO.NET DataAdapter. Elas podem inclusive ser representadas pelo nome de uma stored procedure. Esse recurso do ADO.NET era um dos componentes-chave ausentes no ADO tradicional e que teriam tornado suas técnicas de atualização em lote muito mais flexíveis. Ele requer um pouco mais de codificação durante a fase de design, mas o esforço é compensado pela flexibilidade que você ganha para especificar instruções SQL bastante específicas ou stored procedures usadas com mais freqüência para cada uma das operações.

 

Rowsets Desconectados

É possível desconectar o objeto ADO Recordset de sua fonte de dados por meio da definição adequada de suas propriedades. Uma vez desconectado, ele pode armazenar todo o rowset na memória e ser passado entre os aplicativos que usam ADO tradicional. O código de exemplo a seguir desconectará um Recordset:

 

'—Desconectando um ADO Recordset

oRs.CursorLocation = adUseClient

oRs.CursorType = adOpenStatic

oRs.LockType = adLockBatchOptimistic

oRs.Open

Set oRs.ActiveConnection = Nothing

 

As propriedades-chave envolvidas na desconexão de um Recordset são CursorType e CursorLocation. A propriedade CursorLocation precisa ser definida como adUseClient, que significa que o rowset deve ser armazenado na memória do Recordset. A propriedade CursorType deve ser definida como adOpenStatic, que permite que o cursor do rowset seja movido em qualquer direção mas não permite que o rowset seja automaticamente sensível a alterações no banco de dados subjacente. Combinando essas duas configurações, é possível criar o Recordset desconectado. No entanto, a etapa final ainda está por vir. Uma vez aberto o Recordset, para que ele se torne verdadeiramente desconectado, é necessário definir sua propriedade ActiveConnection como a palavra-chave Nothing, conforme mostrado no último exemplo de código.

O ADO Recordset foi criado para funcionar tanto em modo conectado como em modo desconectado. No entanto, no ADO.NET o DataReader permanece conectado enquanto o DataSet é totalmente desconectado. O DataSet é o sucessor do ADO Recordset desconectado, já que implementa um cursor no lado cliente que pode ser rolado em qualquer direção. Além disso, o ADO.NET DataSet suporta muitos recursos adicionais. O objeto ADO Recordset tradicional oferece recursos desconectados muito limitados, enquanto o objeto ADO.NET DataSet é projetado para ser desconectado. Diferentemente do Recordset, o DataSet pode armazenar e representar vários rowsets dentro de objetos DataTable e até mesmo relacioná-los entre si usando objetos DataRelation. Veja aqui uma maneira comum de criar e preencher um DataSet usando I método Fill do SqlDataAdapter:

 

//-- Criando e preenchendo um ADO.NET DataSet

DataSet oDs = new DataSet("MyDataSet");

oDa.Fill(oDs);

 

O DataSet pode reforçar relações, limitações de chave primária e limitações de chave externa, além de poder implementar expressões de coluna. O ADO Recordset também tem a capacidade de retornar resultados hierárquicos por meio de seus recursos de formatação de dados. Apesar de desses recursos hierárquicos estarem disponíveis, eles requerem o uso do que eu acredito ser uma sintaxe de formatação de dados pouco prática. O ADO.NET DataSet é mais eficiente e intuitivo nesse aspecto porque ele é criado para funcionar com estruturas de dados relacionais, usando seus objetos DataRelation e DataTable.

Nas últimas duas colunas, analisei as várias técnicas usadas com mais freqüência no desenvolvimento do ADO 2.x e demonstrei como convertê-las para ADO.NET. O ADO.NET é a próxima etapa na evolução do ADO 2.x, e muitos recursos encontrados no ADO tradicional foram totalmente refeitos no ADO.NET. Por exemplo, enquanto no ADO 2.x os recursos eram adicionados aos métodos Open e Save para oferecer suporte à XML, o suporte à XML já estava no design do ADO.NET desde o início. Alguns recursos do ADO sofreram modificações consideráveis. Uma delas é que os muitos tipos de cursores do Recordset foram divididos em vários objetos distintos e específicos, tais como DataReader, o DataSet e o DataAdapter. Outros recursos, como as classes connection e command, sofreram modificações menos óbvias. No entanto, no final, o caminho de migração do ADO para o ADO.NET é relativamente objetivo, já que o ADO.NET foi projetado para aproveitar a sua experiência com o ADO tradicional.

 

Download disponível em www.neoficio.com.br/msdn: DataPoints0408.exe (111KB)