Mesmo antes do .NET sempre buscamos utilizar a orientação a objetos e organizar o código de forma a evitar redundância de código. Não apenas pela questão da reutilização de código, mas por questões de manutenção, pois ao fazer manutenção do código passamos a ter toda a lógica centralizada, sem redundâncias.
Um tipo de código que tem muita tendência a ser repetitivo é o código de acesso a banco. Para evitar a repetição deste código cria-se camadas de acesso a dados isolando o código de acesso a dados do código da camada de regras de negócio.
Nos sistemas que desenvolvi em VB 6 e ASP 3 montei uma camada de dados bem versátil e a utilizei em vários sistemas. Quando passei a desenvolver com .NET criei uma camada de dados semelhante para utilização em sistemas .NET .
Em .NET, porém, devido a termos 100% da orientação a objetos, existe muita flexibilidade para a montagem da camada de dados. Assim sendo a forma que tenho utilizado é apenas uma das muitas possíveis para a montarmos esta camada de dados e tenho visto muitas implementações diferentes da camada de dados.
Então a idéia deste artigo é descrever a implementação que tenho utilizado para a camada de dados e porque optei por esta implementação, iniciando assim um bom debate no grupo de usuários devASPNet a respeito dos prós e contras de cada metodologia.
Primeiramente, antes de entrarmos nas características desta camada, vamos estabelecer alguns padrões para esta camada.
Toda a comunicação com o servidor de dados deve ser feita através de stored procedures.
Esse é um tema controverso, com certeza, mas foi isso que assumi como padrão no meu desenvolvimento.
Esse padrão vai de encontro a um oposto que alguns tem utilizado : Utilizar apenas SQL Ansi realizando todo o processamento na camada de negócios.
Então fica a pergunta: Em que casos utilizar um padrão e em que casos utilizar outro padrão?
Stored Procedures
A favor
Qualquer instrução desenvolvida em stored procedures tem melhor performance do que a mesma instrução gerada via aplicação. Isso é característica dos servidores de dados, que utilizam cache e pré-compilação para garantir isso.
Fica mais simples dar manutenção em regras de acesso a dados quando estas estão em stored procedures, deixando o banco de dados mais flexivel.
Contra
Ao migrar de servidor de banco todas as stored procedures precisarão ser reconstruidas.
SQL ANSI
A favor
Independencia de servidor de dados
Contra
Menor aproveitamento dos recursos do servidor de dados
Independência do servidor ou aproveitamento de recursos?
É essa a questão que precisa ser respondida. Em minha opinião, na maioria dos casos já foi feito um grande investimento no servidor de banco de dados. Torna-se então interessante maximizar os resultados deste investimento, utilizando recursos específicos do servidor de dados, utilizando assim as stored procedures.
Porém quando estivermos desenvolvendo produtos para comercialização que precisarão falar com diversos bancos precisaremos da independencia de banco, precisaremos então do SQL Ansi.
Porém:
- Produzindo softwares para comercialização, seria uma vantagem anuncia-los informando que são capazes de aproveitar o máximo do investimento feito em banco de dados pelo cliente. Desta forma seria interessante se a camada de dados fosse realmente re-criada para vários bancos.
- Existem alguns ambientes de produção que necessitam realmente que a camada de dados tire o máximo de proveito do investimento em banco. Já vi ambientes assim em que uma softwarehouse optou por reproduzir a camada de dados sempre que necessário.
- Ao contrário do que se imagina, mesmo com essa arquitetura consegue-se utilizar bancos que não utilizam stored procedures, tal como MySQL. Para esses casos torna-se necessária uma tradução dos nomes das procedures para instruções de banco na camada de dados.
Desta forma opto por fazer sempre o uso de stored procedures, sabendo que em muitos casos de construção de produtos para comercialização valerá mais a pena utilizar SQL ANSI.
Troca de dados entre as camadas
Existe muita polêmica sobre como a troca de dados entre camadas deve ser realizada. Alguns gostam de realizar tal troca com dataSets, outros com objetos personalizados.
Os dataSets são um pouco mais pesados do que a criação de objetos personalizados. Porém os dataSets já possuem todo um mecanismo de controle de atualizações dos registros para quando isto for necessário - e frequentemente é.
Não ha uma resposta definitiva a este ponto. Uma questão importante, porém, é destacar o que não fazer : Não utilize dataReaders para a troca de dados. O dataReader exige que a conexão com o banco se mantenha aberta, então se ele é utilizado para troca de dados a conexão ficará aberta bem mais tempo do que o necessário, gerando perda de escalabilidade.
Nos componentes que montei costumo utilizar o dataSet para entregar dados para o client, mas utilizar coleções mais simples para passar parâmetros para componentes.Independência de banco
Muitas vezes ao montar uma camada de acesso a dados temos que tomar uma difícil decisão : Devemos utilizar alguma camada intermediária de acesso a dados, tal como ODBC ou OLEDB e desta forma simplificar uma futura troca de banco ou devemos utilizar metodologias de acesso direto a nosso servidor para assim ganharmos performance?
A novidade do .NET é que essa decisão não precisa ser tomada : Podemos ter as duas coisas. O .NET possui uma arquitetura de objetos que nos permite acessar bases de dados com providers específicos de cada base - e desta forma muito mais rápidos - e ainda assim conseguir trocar facilmente de base com uma simples reconfiguração.
Começando: A camada de objetos de banco
O primeiro passo é montarmos o componente que nos permitirá ter a independencia de banco ao mesmo tempo em que garantimos o máximo em performance, utilizando os providers específicos de cada banco.
Isso é possível através do uso de interface. As classes que fazem parte de um Data Provider no .NET estão implementadas com base em interfaces. Por exemplo, as classes de conexão (OLEDBCONNECTION, OraConnection, etc,) são todas baseadas na interface IDBConnection.
O fato de todas as classes estarem vinculadas a interfaces comuns permite que os componentes da aplicação apenas façam referência as interfaces e não as classes. Com isso os componentes podem utilizar uma classe connection sem saber exatamente a que Data Provider pertence, apenas utilizando as definições de métodos existentes na interface.
Porém o momento de criação da instância das classes é um momento em que a classe em si precisará ser conhecida. Deverá haver uma inteligência que decida que data provider utilizar com base em alguma configuração. Devemos então centralizar esta inteligência em um único componente. Precisamos criar um componente que seja o responsável por criar objetos de acesso a dados (os objetos de um data provider) e só esse componente saberá qual data provider estamos utilizando.
Esta classe em questão (vamos chama-la de objetosBanco) terá os seguintes métodos:
CriarConexao
CriarCommand
CriarAdapter
Além destes métodos básicos precisaremos de 2 adicionais para simplificar mais o desenvolvimento:
- CriarObjetosBanco: Este método simplifica a criação dos objetos de banco, executando a sequencia de criação de uma conexão, um command e um adapter.
- ObterParametros: Este método é utilizado para derivação de parâmetros, recurso fundamental para a montagem da camada de dados.
Derivação de parâmetros
Quando chamamos stored procedures precisamos saber exatamente quais parametros estas stored procedures irão receber. Se guardarmos esta informação no código então sempre que ocorrer uma alteração nas stored procedures torna-se necessário alterar também o código da camada de dados. A alternativa é realizarmos uma consulta a banco para descobrirmos qual a configuração de parâmetros da procedure. Isso torna a chamada da procedure mais dinâmica porém consome uma consulta a mais ao banco.
Este é o dilema da derivação de parâmetros: ganharmos consideravelmente na manutenção da aplicação sacrificando um pouco a performance.
É importante observar que sem a derivação de parâmetros seriamos obrigados a criar, na camada de acesso a dados, um componente para cada componente de negócio, uma espécie de mapeamento completo da base de dados. Utilizando a derivação de parâmetros evitamos isso.
Por isso na arquitetura que estarei demonstrando aqui estarei optando pelo uso da derivação de parâmetros.
Inicializando a classe
Quando a classe objetosBanco for instanciada ela precisará de imediato descobrir com qual provider ela deve trabalhar, checando as configurações do ambiente.
Para fazer isso devemos programar o constructor da classe, a sub new. Veja o código:
'Abaixo encontra-se a variável e o enum que determinam
'o tipo de banco
Dim iBanco As eTipoBanco
Public Enum eTipoBanco
OLEDB = 1
SQL = 2
End Enum
'Variável contendo a string de conexao
Dim cString As String
'o constructor da classe faz a leitura do tipo de banco
Public Sub New()
Dim app As New System.Configuration.AppSettingsReader
iBanco = app.GetValue("tipobanco", GetType(Integer))
cString = app.GetValue("StringConexao", GetType(String))
End Sub
Utilizando a classe AppSettingsReader fazemos a leitura de configurações do manifesto da aplicação. A aplicação apenas precisará ter um manifesto que contenha as configurações tipobanco (o tipo do provider utilizado) e StringConexao (a string de conexão para o banco de dados.
Precisaremos também de variáveis para guardar os objetos de acesso a banco e podemos utilizar um enum para simplificar o trabalho de escolha do provider. Veja como fica:
Dim cn As IDbConnection
Dim cmd As IDbCommand
Dim da As IDataAdapter
Observe que as variáveis para conter os objetos de banco foram definidas como sendo de uma interface Vejamos como fica o código de criação do objeto Command e do objeto Connection:
Public Function CriarAdapter(Optional ByVal bInterligarCommand As Boolean = False) As IDbDataAdapter
Select Case iBanco
Case eTipoBanco.OLEDB
If Not bInterligarCommand Then
Return (New OleDb.OleDbDataAdapter)
Else
Return (New OleDb.OleDbDataAdapter(cmd))
End If
Case eTipoBanco.SQL
If Not bInterligarCommand Then
Return (New SqlClient.SqlDataAdapter)
Else
Return (New SqlClient.SqlDataAdapter(cmd))
End If
Case Else
Throw New ApplicationException("Erro de configuração na configuração do tipo de banco")
End Select
End Function
Desta forma, conforme o parâmetro recebido o método já faz a interligação entre o Command e o adapter ou não.
Vamos então partir para os métodos auxiliares. Vamos começar pelo CriarObjetosBanco. Veja como fica:
Public Sub ObterParametros(ByVal cmd As IDbCommand)
If TypeOf cmd Is OleDb.OleDbCommand Then
Dim cb As New OleDb.OleDbCommandBuilder
cb.DeriveParameters(cmd)
ElseIf TypeOf cmd Is SqlClient.SqlCommand Then
Dim cb As New SqlClient.SqlCommandBuilder
cb.DeriveParameters(cmd)
Else
Throw New ApplicationException("Não é possível obter os parâmetros deste tipo de command - funcionalidade não implementada")
End If
End Sub
Para fazer a derivação de parâmetros precisamos de um CommandBuilder. Tendo criado este objeto basta fazer uma chamada simples ao método DeriveParameters.
Por fim, faltam apenas as propriedades para permitirem o acesso aos objetos de banco externamente:
Public ReadOnly Property command() As IDbCommand
Get
Return (cmd)
End Get
End Property
_
Public Function obterDadosProcedure(ByVal dt As DataTable, ByVal NomeProcedure As String, Optional ByVal parametros As Hashtable = Nothing) As DataTable
'Cria os objetos de banco
Dim banco As New clObjetosBanco
Try
banco.CriarObjetosBanco()
banco.command.CommandText = NomeProcedure
'Faz a carga dos dados
dt.BeginLoadData()
Dim DTM As New Common.DataTableMapping("Table", dt.TableName)
banco.DataAdapter.TableMappings.Add(DTM)
'Se houverem parâmetros para a procedure, faz o preenchimento
If Not IsNothing(parametros) Then
banco.conexao.Open()
banco.ObterParametros(banco.command)
preencherParametros(banco.command, parametros)
End If
Dim ds As DataSet
ds = dt.DataSet
banco.DataAdapter.Fill(ds)
Catch er As OleDb.OleDbException
Throw New ApplicationException("Ocorreu erro durante o processo de obtenção do dataset!", er)
End Try
banco.conexao.Close()
dt.EndLoadData()
Return (dt)
End Function
O adapter criado pelo componente de objetos de banco precisará preencher a datatable. Então precisaremos adicionar um DataTableMapping ao adapter para fazer a vinculação dos dados de origem com a dataTable que foi recebida como parâmetro.
A coleção de parâmetros é opcional. Se ela não tiver sido transmitida, ignoramos. Se tiver sido transmitida então precisamos fazer a derivação de parâmetros e preencher os parâmetros do command. Para este preenchimento vamos chamar uma sub a parte, preencherParametros.
Feito isso basta realizarmos o Fill do adapter. A interface IdbDataAdapter apenas permite o Fill direto em um dataset, então precisamos recuperar o dataSet a partir da dataTable e só então fazer o Fill.
PreencherParametros
A sub PreencherParametros é privada da camada de acesso a dados. Essa sub irá receber um command e uma hashTable e preencher os parâmetros do command com os valores contidos na HashTable.
Public Sub ExecutarProcedure(ByVal NomeProcedure As String, Optional ByVal Parametros As HashTable)
Dim obj As New clObjetosBanco
Dim cn As IDbConnection
Dim cmd As IDbCommand
cn = obj.CriarConexao
cmd = obj.CriarCommand
cmd.CommandType = CommandType.StoredProcedure
cmd.CommandText = NomeProcedure
cmd.Connection = cn
cn.Open()
If Not isnothing(parametros) Then
obj.ObterParametros(cmd)
preencherParametros(cmd, Parametros)
End If
Try
cmd.ExecuteNonQuery()
Catch er As Exception
Throw New ApplicationException("ocorreu um erro na execução", er)
End Try
cn.Close()
End Sub
Pronto, temos nosso componente para a camada de dados.
Com este componente que acabamos de criar os componentes de negócio poderão fazer acesso a banco de forma genérica, acessando procedures para recuperar dados, atualizar, deletar ou inserir dados.
Vejamos então a criação de um componente de negócio.
Componente de negócios
Na camada de negócios os componentes básicos de cadastramento terão todos os mesmos métodos:
Adicionar
Listar
Deletar
A procedure de inclusão pode realizar também atualização quando o registro já existir, então ficamos com um método a menos.
Mesmo na camada de negócios haverá muito código repetitivo, especialmente para as operações básicas. Podemos então criar uma classe base de negócios e aproveitar a herança no .NET para gerar todas as demais.
Então vamos começar vendo como ficam os métodos Adicionar e Deletar:
Public Sub Adicionar(ByVal hs As Hashtable)
ExecutarProc(getProcAdicionar, hs)
End Sub
Public Sub Deletar(ByVal hs As Hashtable)
ExecutarProc(getProcDeletar, hs)
End Sub
Ambos os métodos irão executar procedures passando parâmetros para elas. Portanto para não duplicar o código criamos um método privado chamado ExecutarProc. Este método recebe como parâmetro o nome da procedure e uma coleção com os parâmetros para a procedure.
Mas observe ainda que não utilizei o nome da procedure nestes dois métodos. Isso porque estamos desenvolvendo uma classe base, para ser herdada, então cada uma das sub-classes deverá especificar o nome de suas próprias stored procedures. No local onde entraria o nome da procedure foram feitas chamadas a diferentes subs privadas. São subs definidas com o mustOverride, da seguinte forma:
Protected MustOverride Function getProcAdicionar() As String
Protected MustOverride Function getProcDeletar() As String
Assim sendo quando formos criar uma classe de negócios bastará herdar desta classe base e especificar o nome das procedures através destas funções.
ExecutarProc
Esta sub é bem simples, apenas faz uso do componente de acesso a banco que já construimos para fazer a execução de uma procedure:
Private Sub ExecutarProc(ByVal nomeProc As String, ByVal hs As Hashtable)
Dim bd As New clDados.clAcessoDados
bd.ExecutarProcedure(nomeProc, hs)
End Sub
Listar
O método listar se difere um pouco dos dois anteriores pois deverá receber um conjunto de dados, portando deverá chamar o método obterDadosProcedure. Conforme vimos anteriormente, este método espera receber uma dataTable, tipada ou não tipada. Então o listar será o responsável por criar esta nova dataTable.
Porém estamos criando uma super classe que será herdada pelas classes de negócio específicas, portanto não devemos criar nesta super classe um dataset específico. Vamos então solucionar este problema da mesma forma que fizemos com os nomes das procedures : Criando uma função com mustOverride que devolva um dataSet.
Public Function Listar(Optional ByVal hs As Hashtable = Nothing) As DataSet
Dim obj As New clDados.clAcessoDados
Dim dt As DataTable
dt = obj.obterDadosProcedure(getDataTable, getProcListar, hs)
obj = Nothing
Return (dt.DataSet)
End Function
Este método Listar utiliza 2 funções com mustOverride, veja como elas ficam:
Protected MustOverride Function getProcListar() As String
Protected MustOverride Function getDataTable() As DataTable
Criando um componente de negócio específico
Vejamos agora como fica o código para a criação de um componente de negócio específico através da classe base que acabamos de criar:
Public Class CadProdutos
Inherits clNegocios.clNegocioBase
Protected Overrides Function getProcAdicionar() As String
Return "stp_adicionar_produtos"
End Function
Protected Overrides Function getProcDeletar() As String
Return "stp_deletar_produtos"
End Function
Protected Overrides Function getProcListar() As String
Return "stp_Listar_Produtos"
End Function
Protected Overrides Function getDataTable() As DataTable
Dim _DataBase As New dsProdutos
Return _DataBase.Tables("tbl_produtos")
End Function
End Class
Conclusão
Esta é apenas uma das formas possíveis para montarmos uma camada de dados e de negócios com .NET. Muitas outras variações podem ser criadas.
O componente da camada de dados pode ser ampliado para oferecer maiores recursos, como por exemplo execuções múltiplas de procedures (inserção de muitos registros), retorno de parâmetros de output, entre outros recursos.
Mostrei apenas a montagem desta arquitetura com relação a acesso a dados. Esses componentes mostrados aqui precisam ainda de tratamento de erros, sistema de logs, etc.
Neste exemplo fiz a transferência de dados entre as camadas com hashTable, mas isso pode variar, pode-se até mesmo utilizar uma DataTable.