ASP .NET - Criando uma componente de acesso a dados (VB.NET) - I
A grande maioria das aplicações profissionais acessam um banco de dados relacional e isso acrescenta uma certa complexidade à etapa de desenvolvimento de software que pode determinar o sucesso ou o fracasso de uma aplicação comercial.
Nas aplicações profissionais o código de acesso ao banco de dados nunca deve estar embutido diretamente na camada de apresentação mas em geral deve estar encapsulado em uma classe dedicada a essa tarefa.
Para realizar uma operação com o banco de dados nessa abordagem o cliente cria uma instância dessa classe e chama o método apropriado. É assim que deve ser feito.(ou pelo menos deveria...)
Neste artigo eu mostro como criar um componente de acesso a dados que segue esse modelo e que deve seguir alguns princípios básicos para que ele seja encapsulado, otimizado e seja executado em um processo separado. Esses princípios básicos são listados a seguir:
Um projeto bom e simples para um componente de acesso a dados deve usar uma classe separada para cada tabela de banco de dados (ou grupo logicamente relacionado de tabelas). Os métodos comuns de acesso ao banco de dados, tais como inserir, excluir e modificar um registro são todos encapsulados em diferentes métodos sem estado. Finalmente, cada chamada de banco de dados utiliza um procedimento armazenado específico. Abaixo temos uma figura que mostra um esquema desse tipo de projeto em camadas:
O exemplo que vamos mostrar neste artigo demonstra um componente de banco de dados simples. Em vez de colocar o código de banco de dados na página web, seguimos uma prática muito melhor de projeto de separar o código em uma classe distinta que pode ser usada em várias páginas. Esta classe pode então ser compilada como parte de um componente separado, se necessário. Além disso, a string de conexão é recuperada a partir da seção <connectionStrings> do arquivo web.config, ao invés de ser colocada diretamente no código.
O nosso componente de acesso a dados será constituído de duas classes, uma classe de pacote de dados que envolve um único registro de informações (conhecida como a classe de dados) e uma classe de utilitário de banco de dados que realiza as operações de banco de dados via código ADO.NET (conhecida como a classe de acesso aos dados).
Por questão de simplicidade vamos usar um banco de dados conhecido: o Northwind.mdf, e, criar um componente para acessar esse banco de dados. Com base nisso vamos criar uma classe que acessa a tabela Employees e que representa os funcionários da empresa Northwind.
Apenas para lembrar segue abaixo a estrutura dessa tabela Employees:
Abra o Visual Web Developer 2010 Express Edition e crie um novo projeto do tipo Class Library com o nome Employee.vb incluído o código abaixo neste arquivo:
Public Class Employee Private m_employeeID As Integer Private m_firstName As String Private m_lastName As String Private m_titleOfCourtesy As String Public Property EmployeeID() As Integer Get Return m_employeeID End Get Set(ByVal value As Integer) m_employeeID = value End Set End Property Public Property FirstName() As String Get Return m_firstName End Get Set(ByVal value As String) m_firstName = value End Set End Property Public Property LastName() As String Get Return m_lastName End Get Set(ByVal value As String) m_lastName = value End Set End Property Public Property TitleOfCourtesy() As String Get Return m_titleOfCourtesy End Get Set(ByVal value As String) m_titleOfCourtesy = value End Set End Property Public Sub New(ByVal employeeID As Integer, ByVal firstName As String, ByVal lastName As String, ByVal titleOfCourtesy As String ) Me.m_employeeID = employeeID Me.m_firstName = firstName Me.m_lastName = lastName Me.m_titleOfCourtesy = titleOfCourtesy End Sub End Class |
Observe que esta classe não inclui todas as informações que estão na tabela Employees, a fim de tornar o exemplo mais conciso, definimos apenas algumas propriedades. Ao construir uma classe de dados, você pode optar por utilizar o novo recurso das propriedades automáticas e assim simplificar o código. Abaixo temos uma declaração usando este recurso para a propriedade EmployeeID:
Public Property EmployeeID As Integer
Ao utilizar propriedades automáticas, a variável privada é gerada automaticamente em tempo de compilação, assim você não vai saber o seu nome. Em seu código, você deve sempre acessar a variável privada através da procedimentos de propriedade. O compilador VB também adiciona o código que obtém e define a variável privada. Nosso exemplo usa a sintaxe antiga.
Definindo as Stored Procedures
Vamos agora criar os procedimentos armazenados que iremos usar em nosso componente. Para isso vamos usar o SQL Server Management Studio 2010 Express Edition (SSMS).
Obs: O meu SSMS esta localizado para português e por isso as mensagens estarão todas em português.
Após abrir o SSMS e efetuar o login vamos selecionar o banco de dados Northwind.mdf e o item Programação clicando sobre ele com o botão direito do mouse e selecionando a opção - Novo Procedimento Armazenado;
A seguir digite o código mostrada na figura abaixo para criar a stored Procedure InserirEmployee que inclui um novo funcionário na tabela Employees:
Para retornar o valor de um
campo identity de uma nova linha incluída no SQL Server usamos
uma stored procedure com um parâmetro de saída (OUTPUT
Parameter).
Para realizar estas tarefas podemos usar três funções Transact-SQL
no SQL Server:
Função Descrição :
Vamos repetir o procedimento acima e criar as seguintes stored procedures que serão usadas em nosso componente:
- DeletaEmployee:
CREATE PROCEDURE DeletaEmployee @EmployeeID int AS DELETE FROM Employees WHERE EmployeeID = @EmployeeID GO |
- AtualizaEmployee:
CREATE PROCEDURE AtualizaEmployee @EmployeeID int, @TitleOfCourtesy varchar(25), @LastName varchar(20), @FirstName varchar(10) AS UPDATE Employees SET TitleOfCourtesy = @TitleOfCourtesy, LastName = @LastName, FirstName = @FirstName WHERE EmployeeID = @EmployeeID GO |
- GetTodosEmployees:
CREATE
PROCEDURE GetTodosEmployees AS SELECT EmployeeID, FirstName, LastName, TitleOfCourtesy FROM Employees GO |
- ContaEmployees
CREATE PROCEDURE ContaEmployees AS SELECT COUNT(EmployeeID) FROM Employees GO |
- GetEmployee
CREATE PROCEDURE GetEmployee @EmployeeID int AS SELECT EmployeeID, FirstName, LastName, TitleOfCourtesy FROM Employees WHERE EmployeeID = @EmployeeID GO |
Ao final deveremos visualizar as stored procedures criadas no item Programação conforme abaixo:
Definindo a classe de acesso aos dados
Agora que já temos as stored procedures no banco de dados vamos criar a classe utilitária que realizará as operações no banco de dados. Essa classe usa os procedimentos armazenados que criamos anteriormente. Neste exemplo, a classe de utilitário de dados terá o nome EmployeeDB. Ela encapsula todo o código de acesso a dados ao banco de dados.
A seguir temos um esboço da estrutura da nossa classe EmployeeDB :
Imports System.Configuration Imports System.Data.SqlClient Imports System.Data Public Class EmployeeDB Private connectionString As String Public Sub New() 'Obtém uma string de conexão do web.config. connectionString = ConfigurationManager.ConnectionStrings("Northwind").ConnectionString End Sub Public Sub New(ByVal connectionString As String) ' Define uma string de conexão Me.connectionString = connectionString ..... End Sub Public Function InserirEmployee(ByVal emp As Employee) As Integer End Function Public Sub DeletaEmployee(ByVal employeeID As Integer) End Sub Public Sub AtualizaEmployee(ByVal emp As Employee) End Sub Public Function GetEmployee(ByVal employeeID As Integer) As Employee End Function Public Function GetEmployees() As List(Of Employee) End Function Public Function ContaEmployees() As Integer End Function End Class |
Observe que a classe EmployeeDB usa métodos de instância, e não métodos estáticos ou Shared. Isso ocorre porque, embora a classe EmployeeDB não armazene qualquer estado do banco de dados, ela armazena a string de conexão como uma variável de membro privado. Como essa classe é uma classe de instância, a string de conexão pode ser recuperada toda vez que a classe é criada, ao invés de quando cada vez que um método for invocado. Essa abordagem torna o código mais claro e permite que ele seja um pouco mais rápido (evitando a necessidade de ler o arquivo web.config muitas vezes). No entanto, o benefício é pequeno, assim poderíamos usar métodos estáticos na nossa classe sem grandes impactos.
Temos que incluir uma referência no projeto no menu Project -> Add Reference e na guia .NET selecionar a opção System.Configuration.
Dessa forma podemos usar a declaração Imports System.Configuration no arquivo bem como as declarações para acessar as classes do provider SqlClient.
Vamos começar definindo o método InserirEmployee() com o código abaixo:
Public Function InserirEmployee(ByVal emp As Employee) As Integer Dim con As New SqlConnection(connectionString) Dim cmd As New SqlCommand("InserirEmployee", con) cmd.CommandType = CommandType.StoredProcedure cmd.Parameters.Add(New SqlParameter("@FirstName", SqlDbType.NVarChar, 10)) cmd.Parameters("@FirstName").Value = emp.FirstName cmd.Parameters.Add(New SqlParameter("@LastName", SqlDbType.NVarChar, 20)) cmd.Parameters("@LastName").Value = emp.LastName cmd.Parameters.Add(New SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25)) cmd.Parameters("@TitleOfCourtesy").Value = emp.TitleOfCourtesy cmd.Parameters.Add(New SqlParameter("@EmployeeID", SqlDbType.Int)) cmd.Parameters("@EmployeeID").Direction = ParameterDirection.Output Try con.Open() cmd.ExecuteNonQuery() Return CInt(cmd.Parameters("@EmployeeID").Value) Catch err As SqlException ' Substitui o erro com algo menos especifico Throw New ApplicationException("Erro ao acessar os dados..") Finally con.Close() End Try End Function |
O método aceita dados como um objeto de dados Employee. Quaisquer erros são capturados, e os detalhes internos não são retornados ao código de página da web. Isso impede que a página web forneça informações que poderiam levar a possíveis explorações mal intencionadas. Este seria também um local ideal para chamar um outro método em um componente de log para relatar as informações completas em um log de eventos ou em outro banco de dados.
Os métodos GetEmployee() e GetEmployees() retornam os dados utilizando um único objeto Employee objeto ou uma lista de objetos Employee; a seguir temos a implementação destes métodos:
- GetEmployee() - retorna um objeto Employee
Public Function GetEmployee(ByVal employeeID As Integer) As Employee Dim con As New SqlConnection(connectionString) Dim cmd As New SqlCommand("GetEmployee", con) cmd.CommandType = CommandType.StoredProcedure cmd.Parameters.Add(New SqlParameter("@EmployeeID", SqlDbType.Int)) cmd.Parameters("@EmployeeID").Value = employeeID Try con.Open() Dim reader As SqlDataReader = cmd.ExecuteReader(CommandBehavior.SingleRow) ' Obtem a primeira linha reader.Read() Dim emp As New Employee( CInt(reader("EmployeeID")), DirectCast(reader("FirstName"), String), DirectCast(reader("LastName"), String), DirectCast(reader("TitleOfCourtesy"), String) ) reader.Close() Return emp Catch err As SqlException ' Retorna um erro menos especifico Throw New ApplicationException("Erro ao acessar os dados.") Finally con.Close() End Try End Function |
- GetEmployees() - retorna uma lista de objetos Employee
Public Function GetEmployees() As List(Of Employee) Dim con As New SqlConnection(connectionString) Dim cmd As New SqlCommand("GetTodosEmployees", con) cmd.CommandType = CommandType.StoredProcedure ' Cria uma coleção para todos os registros employee Dim employees As New List(Of Employee)() Try con.Open() Dim reader As SqlDataReader = cmd.ExecuteReader() While reader.Read() Dim emp As New Employee( CInt(reader("EmployeeID")), DirectCast(reader("FirstName"), String), DirectCast(reader("LastName"), String), DirectCast(reader("TitleOfCourtesy"), String) ) employees.Add(emp) End While reader.Close() Return employees Catch err As SqlException ' Retorna um erro menos especifico Throw New ApplicationException("Data error.") Finally con.Close() End Try End Function |
O método AtualizaEmployee() é importante pois vai determinar a estratégia do tratamento da concorrência em nossa aplicação.
Em qualquer aplicativo multiusuário, incluindo aplicações web, existe a possibilidade de que mais de um usuário realize a sobreposição de consultas e atualizações. Isso pode levar a uma situação potencialmente confusa onde dois usuários, que estão da posse do estado atual de uma linha, tentem realizar atualizações divergentes.
A atualização do primeiro usuário sempre terá êxito. O sucesso ou o fracasso da segunda atualização é determinada pela estratégia de concorrência.
Existem várias abordagens para a gestão da concorrência. A coisa mais importante a entender é que você determina a sua estratégia de concorrência pela forma como você define a atualização dos seus dados via comandos SQL UPDATE e DELETE (particularmente a forma como você define a cláusula WHERE).
Vejamos os métodos mais usados:
1-) Atualização Last-in-wins - Esta é uma forma menos restritiva de controle de concorrência que sempre confirma a atualização (a menos que a linha original tenha sido deletada).
Nessa abordagem toda vez que uma atualização é confirmada, todos os valores são aplicados. Esse modelo faz sentido se colisões de dados são raros. Ex: Você pode usar com segurança esta abordagem se houver apenas uma pessoa responsável pela atualização de um determinado grupo de registros. Em geral você implementa o modelo Last-in-wins escrevendo uma cláusula WHERE que corresponde ao registro a ser atualizado com base na sua chave primária.
O método AtualizaEmployee() utiliza a abordagem Last-in-wins e define a atualização da seguinte forma: UPDATE Employees SET ... WHERE EmployeeID=@EmployeeID
2-) Atualização Match-all - Para implementar esta estratégia, o seu comando UPDATE precisa usar todos os valores que você deseja definir, além de todos os valores originais. Você usa todos os valores originais para construir a cláusula WHERE que encontre o registro original. Dessa forma, se até mesmo um único campo tiver sido modificado, o registro não vai corresponder e a mudança não será bem sucedida. Um problema com este abordagem é que as mudanças compatíveis não são permitidas. Por exemplo, se dois usuários estão tentando modificar diferentes partes do mesmo registro, a mudança do segundo usuário será rejeitada, mesmo embora não esteja em conflito. Outro problema mais significativo com a atualização essa estratégia é que ela leva a grandes declarações SQL ineficientes. Você pode implementar a mesmo estratégia de forma mais eficaz usando timestamps.
Exemplode de declaração SQL com essa estratégia: UPDATE Employees SET ... WHERE EmployeeID=@EmployeeID AND FirstName=@OriginalFirstName AND LastName=@OriginalLastName ...
3-) Atualização baseada em timestamp - A maioria dos sistemas de banco de dados suporta uma coluna timestamp, que a fonte de dados atualiza automaticamente a cada vez que uma mudança é realizada. Você não modifica a coluna timestamp manualmente. No entanto, se você retornar o seu valor quando você executar sua instrução SELECT, você pode usá-la na cláusula WHERE para a instrução UPDATE. Dessa forma, você está garantindo a atualização do registro somente se ele não foi modificado, assim como na abordagem match-all. Ao contrário de atualização match-all, a cláusula WHERE é menor e mais eficiente, porque ele só precisa de dois pedaços de informação: a chave primária e o timestamp.
Exemplo de instrução SQL com essa abordagem: UPDATE Employees SET ... WHERE EmployeeID=@EmployeeID AND TimeStamp=@TimeStamp
4-) Atualização baseada na alteração de valores dos campos - Esta abordagem tenta aplicar apenas os valores alterados em um comando UPDATE, permitindo assim que dois usuários façam alterações ao mesmo tempo, se estas mudanças são para campos diferentes. O problema com esta abordagem é que ela pode se tornar muito complexa, porque você precisa se ??manter a par dos valores que mudaram (caso em que devem ser incorporadas na cláusula WHERE) e dos valores que não sofreram alteração.
A abordagem last-in-wins é um exemplo de acesso de dados sem controle de concorrência. atualização Match-all, atualização baseada no timestamp e a atualização baseada na mudança de valores dos campos são exemplos de concorrência otimista.
Na concorrência otimista o código não mantém bloqueios sobre os dados que ele está usando, em vez disso, sua estratégia é esperar que as mudanças não se sobreponham respondendo de acordo se elas ocorrerem.
A concorrência pessimista, utilizada em transações, evita conflitos de simultaneidade travando os registros em uso. A desvantagem é a escalabilidade, pois outros usuários que tentam acessar os mesmos dados deverão aguardar até a liberação do bloqueio.
Abaixo temos o método AtualizaEmployee:
Public Sub AtualizaEmployee(ByVal EmployeeID As Integer, ByVal firstName As String, ByVal lastName As String, ByVal titleOfCourtesy As String) Dim con As New SqlConnection(connectionString) Dim cmd As New SqlCommand("AtualizaEmployee", con) cmd.CommandType = CommandType.StoredProcedure cmd.Parameters.Add(New SqlParameter("@FirstName", SqlDbType.NVarChar, 10)) cmd.Parameters("@FirstName").Value = firstName cmd.Parameters.Add(New SqlParameter("@LastName", SqlDbType.NVarChar, 20)) cmd.Parameters("@LastName").Value = lastName cmd.Parameters.Add(New SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25)) cmd.Parameters("@TitleOfCourtesy").Value = titleOfCourtesy cmd.Parameters.Add(New SqlParameter("@EmployeeID", SqlDbType.Int)) cmd.Parameters("@EmployeeID").Value = EmployeeID Try con.Open() cmd.ExecuteNonQuery() Catch err As SqlException ' Substitui o erro com algo menos especifico Throw New ApplicationException("Erro ao acessar os dados.") Finally con.Close() End Try End Sub |
- O método DeletaEmployee()
Public Sub DeletaEmployee(ByVal employeeID As Integer) Dim con As New SqlConnection(connectionString) Dim cmd As New SqlCommand("DeletaEmployee", con) cmd.CommandType = CommandType.StoredProcedure cmd.Parameters.Add(New SqlParameter("@EmployeeID", SqlDbType.Int)) cmd.Parameters("@EmployeeID").Value = employeeID Try con.Open() cmd.ExecuteNonQuery() Catch err As SqlException ' Retorna um erro menos especifico Throw New ApplicationException("Erro ao acessar os dados.") Finally con.Close() End Try End Sub |
- O método ContaEmployee()
Public Function ContaEmployees() As Integer Dim con As New SqlConnection(connectionString) Dim cmd As New SqlCommand("ContaEmployees", con) cmd.CommandType = CommandType.StoredProcedure Try con.Open() Return CInt(cmd.ExecuteScalar()) Catch err As SqlException ' Retorna um erro menos especifico Throw New ApplicationException("Erro ao acessar os dados.") Finally con.Close() End Try End Function |
Assim acabamos de criar todos os métodos do nosso componente de acesso a dados para gerenciar os dados da tabela Employees vamos agora criar a aplicação ASP .NET Web Forms que irá usar o componente.
Aguarde a segunda parte deste artigo: ASP .NET - Usando o componente de acesso a dados (VB.NET) - II
Salmos 7:1
Senhor, Deus meu, confio, salva-me de todo o que me persegue, e livra-me;Referências: