.NET - Uma reflexão sobre o desenvolvimento com acesso a dados - I


A era dos bancos de dados relacionais, recordsets e datasets

Quando vamos desenvolver uma aplicação que vai tratar com dados temos que decidir como acessar e representar esse dados. Esta decisão é provavelmente a mais importante que faremos em termos do desempenho e facilidade de desenvolvimento e manutenabilidade da aplicação.

Em geral o mecanismo de persistência adota é um banco de dados relacional. A despeito da tentativa de introduzir banco de dados para objetos os bancos de dados relacionais ainda é e será por muitos anos o principal mecanismo de persistência usado nas aplicações.

Até o momento os bancos de dados relacionais oferecem todas as funcionalidades que precisamos para persistir e retornar dados.

Podemos dizer que quando precisamos armazenar permanentemente dados, os bancos de dados relacionais são a melhor opção.

Por outro lado quando temos que representar temporariamente os dados em uma aplicação objetos são a melhor opção que temos. Características como herança, encapsulamento e sobrecarga de métodos permitem a utilização de código melhor e mais simples se comparado com a abordagem usada pelo DataSet.

O DataSet é um objeto que representa os dados em memória e se parece com o antigo objeto recordset no que se refere ao propósito e na representação interna dos dados visto que tanto no recordset como no dataset os dados são representados em linhas e colunas nas tabelas.

Embora essa representação seja eficiente em alguns cenários é peca pela falta de muitas funcionalidades como tipos seguros, desempenho e maleabilidade.

Durante a última década, temos desenvolvido aplicações utilizando VB6, ASP, Delphi, VB .NET e C#, usando componentes externos ou objetos para acessar bancos de dados e manter os dados internamente.

Ambas as tarefas são semelhantes em cada linguagem usada, mas elas são especialmente semelhantes na representação dos dados internos, onde os dados são organizados em estruturas construídas sobre o conceito de linhas e colunas. O resultado é que as aplicações que realizam o gerenciamento dos dados o fazem da mesma forma que eles são organizados no banco de dados.

Por que diferentes fornecedores oferecem aos desenvolvedores o mesmo modelo de programação ?

A resposta é simples: os desenvolvedores estão acostumados à representação tabular, e não têm necessidade de aprender mais nada para serem produtivos.

Além disso, essas estruturas genéricas podem conter quaisquer dados, desde que eles possam ser representados em linhas e colunas e mesmo dados provenientes de arquivos XML, web services podem ser organizados desta forma.

Uma das conseqüências desta postura é que os vendedores desenvolveram um subconjunto de objetos que podem representar qualquer informação sem que seja preciso escrever muitas linhas de código. Esses objetos são chamados contêiners ou contentor de dados.

Usando DataSets e DataReaders como Contêiner de dados

Se você acompanhou a plataforma .NET desde o seu início e desenvolveu aplicações para acesso a dados. Com certeza já utilizou datasets e datareaders.

Eles eram muito convenientes pois com poucas linhas de código tínhamos um objeto que poderíamos vincular a muitos controles de dados vinculados e que , no caso do datareader, oferecia um bom desempenho.

Por outro lado usando um data adapter em combinação com um dataset tínhamos uma representação completa do banco de dados em memória onde poderíamos realizar operações de leitura e atualização de dados.

Nunca tínhamos sido tão produtivos... (santa ingenuidade...)

Com a ajuda dos assistentes da plataforma .NET e da integração destes objetos tínhamos a sensação que poderíamos criar qualquer aplicação usando apenas o recurso arrastar-soltar e algumas linhas de código.

Você Concorda ???

Vamos a um exemplo prático.

Suponha que você tenha um banco de dados com duas tabelas : Pedidos e DetalhesPedido com a seguinte estrutura:

Obs: As tabelas foram extraídas do banco de dados Northwind.mdf

Suponha que você tenha que exibir todos os pedidos em uma aplicação.

Era relativamente simples bastava fazer o seguinte:

  1. Criar uma conexão com o banco de dados;
  2. Criar um Adapter;
  3. Executar uma consulta e preencher um data table;
  4. Vincular o data table a um controle de visualização como um ListView, DataGridView, etc.

O código seria simples:


using (SqlConnection conn = new SqlConnection(connString))
{
 using (SqlDataAdapter da = new SqlDataAdapter("Select * from order", conn))
 {
    DataTable dt = new DataTable();
    da.Fill(dt);
    ListView1.DataSource = dt;
    ListView1.DataBind();
 }
}
Using conn As New SqlConnection(connString)
   Using da As New SqlDataAdapter("Select * from order", conn)
     Dim dt As New DataTable() 
     da.Fill(dt)
     ListView1.DataSource = dt
     ListView1.DataBind()
   End Using
End Using
C# VB .NET

Mas seria esse modelo o ideal em termos de produtividade ?

Teria o desenvolvedor condições de criar aplicações robustas, de fácil manutenção e reutilização usando este modelo ?

Vamos fazer uma análise dessa abordagem...

A maneira como o acesso a dados é realizado é completamente inseguro e genérico.

Por um lado, você tem uma grande flexibilidade, porque você pode facilmente escrever código genérico para implementar funções que desconhecem os nomes dos campos da tabela e dependem da configuração.

Por outro lado, você perde a segurança de tipos. Você identifica um campo especificando seu nome através de strings , se o nome não é correto, você obtém uma exceção somente em tempo de execução.

Você perde controle não somente sobre os nomes dos campos, mas também sobre os tipos de dados. Os DataReaders e DataTables (que são os itens que contém os dados em um DataSet) retornam os valores das colunas como do tipo Object (o tipo de dados base da plataforma .NET), de forma que você precisa realizar uma conversão forçada, um cast, os valores para o tipo correto (ou invocar o método ToString quando pertinente).

DataReaders e DataTables não permitem a recuperação de dados de forma transparente sem afetar a interface com o usuário. E isso não é um bom sinal. Significa que seu aplicativo esta fortemente acoplado com a estrutura do banco de dados e qualquer mudança na estrutura requer que seu código também seja alterado.

Dessa forma temos que o código da interface esta precisando ser adaptado para refletir as mudanças e esta é a razão mais importante porque a utilização destes objetos é desencorajada.

Mesmo se você tiver os mesmos dados na memória, como eles são retornados afeta como eles são representados internamente. E este problema não deveria ser tratado na interface do usuário e sim no código de acesso aos dados.

Para retornar um valor de uma coluna armazenada em um DataReader ou DataTable geralmente referenciamos a coluna usando uma constante string. Ex:

object endereco = orders.Rows[0]["Endereco"];
Dim endereco As Object = orders.Rows(0)("Endereco")
C# VB .NET

A variável endereco é do tipo System.Object, por isso pode conter potencialmente qualquer tipo de dados. Você pode até saber que ela contém um valor string, mas para usá-lo como uma string, você tem que executar explicitamente um casting ou operação de conversão da seguinte forma:

string _endereco = (string)orders.Rows[0]["Endereco"];
string _endereco = orders.Rows[0]["Endereco"].ToString();
Dim _endereco As String = DirectCast(orders.Rows(0)("Endereco"), String)
Dim _endereco As String = orders.Rows(0)("Endereco").ToString()
C# VB .NET

Realizar uma conversão ou casting tem um custo em termos de desempenho e uso de memória porque converter de um tipo de valor para um tipo de referência e vice-versa faz com que ocorra o boxing e unboxing. Em alguns casos, a conversão pode requerer o uso da interface IConvertible, o que causa um casting interno.

Os DataReaders têm uma vantagem sobre os DataTable. Eles oferecem métodos tipados para acessar campos sem precisar de conversões explícitas. Tais métodos aceitam um parâmetro inteiro que representa o índice da coluna na linha. Os DataReaders também tem um método que retorna o índice de uma coluna, dado o seu nome, mas seu uso tende a desordenar o código e está sujeita a erros de digitação.

O DataSet é provavelmente uma das estruturas mais complexas na biblioteca de classes. NET.; ele contém uma ou mais instâncias DataTable, e cada uma delas tem uma lista de objetos DataRow feitos de um conjunto de DataColumnobjects. Um DataTable pode ter uma chave primária composta por uma ou mais colunas e pode declarar que determinadas colunas têm uma relação de chave estrangeira com colunas em outro DataTable.

As colunas suportam o versionamento, o que significa que se você alterar o valor, tanto o velho como o novo valor são armazenados na coluna para executar verificações de simultaneidade. Para enviar atualizações para o banco de dados, você tem que usar uma classe DbDataAdapter (ou, mais precisamente, uma das suas classes derivadas), que é ainda outro objeto.

Esses recursos são muitas vezes inúteis e são ignorados pelos desenvolvedores; o DataSet internamente cria uma coleção vazia desses objetos. Isto poderia ser um consumo insignificante de recursos para um aplicativo independente, mas em um ambiente multiusuário com milhares de pedidos, como uma aplicação web, se torna inaceitável. É inútil otimizar o desempenho do banco de dados, fazer ajustes de índices, modificar o SQL, acrescentar sugestões, e assim por diante, se você desperdiçar recursos criando estruturas que você não precisa.

Em contraste, um DataReader é construído para diferentes cenários. Um DataTable obtém todos os dados lidos do banco de dados na memória, mas muitas vezes você não precisa de todos os dados em memória e poderia buscá-la registro por registro no banco de dados.

Outra situação é nas atualizações de dados; muitas vezes você precisa ler dados, mas não precisa atualizá-lo. em tal caso, algumas características, como a versão de linha, são inúteis.

DataReader é a melhor escolha na tais situações, porque ele recupera dados em um caminho read-only (mais rápido). Embora aumente o desempenho, o DataReader ainda pode sofrer de problemas de casting e conversão de DataSet, mas esta perda de tempo é menor que o ganho que você recebe do seu uso.

Todos esses problemas podem parecer intimidadores, mas muitas aplicações se beneficiam do uso de banco de dados como estruturas apesar das mazelas apresentadas.

No entanto, em projetos corporativos, onde a base de código é grande e você precisa de mais controle e flexibilidade, você pode aproveitar o poder da programação orientada a objetos e das classes para organizar seus dados.

Pode ainda usar um OR/M para fazer o trabalho pesado de mapeamento e gerenciamento de entidades e  não ter que se preocupar mais com objetos ADO .NET nem com instruções SQL.

Na continuação deste artigo vou falar sobre a utilização dos objetos para representar e persistir dados.

Aguarde : .NET - Uma reflexão sobre o desenvolvimento com acesso a dados II

Lucas 12:4 Digo-vos, amigos meus: Não temais os que matam o corpo, e depois disso nada mais podem fazer.
Lucas 12:5
Mas eu vos mostrarei a quem é que deveis temer; temei aquele que, depois de matar, tem poder para lançar no inferno; sim, digo, a esse temei.

Referências:


José Carlos Macoratti