Banco de Dados - SQL Server

Utilização de Transações de Banco de Dados no .NET Framework

Neste artigo você aprenderá a utilizar transações de banco de dados nas suas aplicações, deixando-as mais consistentes e profissionais.

por Marco Aurélio Péres



Introdução

Nos dias de hoje, todas pessoas que trabalham com informática, sabem de uma maneira geral, que aplicativos de computador manipulam dados.
Pra falar verdade, COMPUTADORES manipulam dados.

Que tipo de manipulação é esta? Em geral, inserções, alterações e exclusões de dados..

Agora, para os desenvolvedores é relativamente fácil escrever códigos que façam isto (ainda mais com o Microsoft Visual Studio .NET), mas existe uma questão que é negligenciada e por isto não deixam as aplicações 100% íntegras.

A questão dita acima é o conceito de "Transações nos Bancos de Dados", e este artigo irá explicar e demonstrar como é fácil trabalhar com Transactions no Microsoft .NET Framework.

1 - Explanação do conceito de Transação

1.1 - EXISTE UM PROBLEMA...
Quando um aplicativo tenta executar um comando em um banco de dados, este comando pode não ser executado de maneira correta, por alguns dos simples motivos abaixo:

  • Erro de parse em todo o comando. A sintaxe SQL (Structured Query Language) não estava correta.
  • Erro de parse em apenas uma parte do comando.
  • Execução parcial. Um dos recursos do banco de dados ficou indisponível durante a execução do comando, não permitindo que todo o comando seja executado.
Quando ocorre um erro, e este impossibilita a total execução do comando/conjunto de comandos, temos um erro não tão crítico quanto um erro que impossibilita APENAS UMA PARTE do comando de ser executado.

Porque ?
Por que se apenas uma parte dos dados que deveriam ser alterados, foram de fato alterados então temos uma inconsistência dos dados no banco. Uma inconsistência de dados vai totalmente contra a regra de negócio estabelecida para a aplicação.

É por isto que eu digo "quando nada é alterado, é melhor", pois a regra de negócios do aplicativo não foi burlada, trazendo resultados inimagináveis para o usuário do aplicativo.

Não conheço exemplo melhor do que a transferência de dinheiro de uma conta para outra.
Suponha que o João entre no Internet Banking e vá transferir R$ 200,00 para a conta do Pedro.
Esta operação envolve 2 tarefas:
  • a) Debitar R$ 200,00 na conta do João.
  • b) Creditar R$ 200,00 na conta do Pedro.
Para que a operação seja bem sucedida, as 2 tarefas devem ser executadas. Imagine as consequências se apenas A fosse executada, ou se apenas B.

Esta situação fica ainda pior, quando os comandos viram conjunto de comandos, ou seja, linhas e mais linhas de códigos SQL que dependem uma da outra, sendo executadas uma após a outra.

1.2 - A SOLUÇÃO...
Para ajudar a resolver este problema, os bancos de dados criaram o conceito de "Transação".
Uma transação é um conjunto de comandos (ou apenas 1 comando) que possui as seguintes características:

Atômico
Todos os comandos dentro de uma transação são executados e tratados de uma maneira única.
Isto é, todos os comandos são executados com sucesso ou nenhum deles é executado. Não é possível que apenas uma parte do conjunto de comandos seja executada.

Consistente
Sua aplicação passará a ser íntegra, quando utiliza transações, por que as transações não permitem que o banco de dados fique inconsistente, contrário a sua regra de negócios (é claro, que os comandos dentro da transação devem estar de acordo com a regra de negócio).

Isolado
Uma transação pode ser completamente isolada de outras transações.
Podem ser executadas independentemente de outras transações estarem sendo executadas no mesmo momento, a ponto de serem vistas pelo banco de dados como se somente elas existissem.

Os bancos de dados suportam vários níveis de isolatamento para uma transação, que serão explicados no tópico "Aplicabilidade no Microsoft .NET Framework".

Durável
Suponha que sua aplicação esteja com muito azar, MAS MUITO MESMO, para acontecer a seguinte situação.
Todos os comandos da transação são executados com sucesso, e o banco de dados vai começar a efetuar as alterações nas tabelas, mas neste meio o computador dá um erro. Isto é, a transação foi executada, mas os dados não foram atualizados, ou apenas uma parte deles.

De uma maneira fantástica, o banco de dados consegue reverter este cenário e garantir ao desenvolvedor que os dados continuarão consistentes.

1.3 - CONCORRÊNCIA DE DADOS
Tudo seria perfeito, se não existisse o fato de que várias transações podem ser executadas no mesmo momento e estarem utilizando o mesmo conjunto de dados.
Devido à este fato, podem acontecer os seguintes erros durante a execução de uma transação, dependendo do tipo/nível de isolamento utilizado.

A - "Dirty-Read" ou "Leitura suja"
A figura 1.a abaixo demonstra este problema.
Quando uma transação efetuar o SELECT no customerID="ALFKI", o banco de dados retornará "Maria Anders" e "Sales Representative". Acontece que no mesmo momento, uma transação de UPDATE neste registro foi confirmada(commit), e o banco de dados está prestes a atualizar o registro.

Resultado: A transação 1 terá o dado "Sales Representative", e o banco terá o dado "Executive Director" no registro "ALFKI". Por isto, este erro é chamado "Leitura Suja", pois ele leu algo que não havia sido confirmado ainda.

fig. 1.a.: Problema "Dirty-Read" em ação.B - "Non-Repeatable Read" ou "Leitura Rrepetida não Permitida"

Este problema é bem parecido com o "Dirty Read", e funciona da seguinte maneira. Acompanhe com a figura 1.b ilustrativa.
A 1º transação efetua um SELECT e tem como resultado "Sales Representative", e a transação continua a ser executada.
Em determinado ponto, acontece 2 coisas:
a - Uma 2º transação "pega" este registro "ALFKI" e efetua um UPDATE, mas ainda não confirmou(commit) os dados.
b - A 1º transação, por algum motivo qualquer, precisa REPETIR a consulta, entretanto, o banco de dados irá retornar "Executive Director", isto é, um valor de diferente de "Sales Representative" que foi retornado no primeiro SELECT da mesma transação.

Resultado: Se não for bem tratado, a transação terá comportamentos inesperados.
fig. 1.b.: Problema "Non-Repeatable Read" em ação.

C - "Phantom Read" ou "Leitura Fantasma"

No problema "Non-Repeatable Read", tivemos 1 registro que foi alterado enquanto outra trasação utilizava ele. O "Phantom Read" é o mesmo conceito, mas ao invés do registro ser alterado, ele será excluído.. Assim a 1º transação terá um registro-fantasma.

Resultado: Na próxima vez que a 1º transação ler o registro, ele não existirá, ocasionando possíveis problemas.
fig. 1.c.: Problema "Phantom Read" em ação.

1.4 - SOLUÇÃO PARA A CONCORRÊNCIA DE DADOS
Antes de mostrar a solução, é importante que você saiba como uma transação é processada.
Veja o código T-SQL abaixo:

BEGIN TRANSACTION;

SELECT ContactName FROM Customers WHERE CustomerID = "ALFKI";
UPDATE Customers SET ContactName = "Marco Aurélio Péres" WHERE CustomerID = "ALFKI";
SELECT ContactName FROM Customers WHERE CustomerID = "ALFKI";

ROLLBACK TRANSACTION;

SELECT ContactName FROM Customers WHERE CustomerID = "ALFKI";

Está uma transação muito simples. Tudo que está entre "BEGIN TRANSACTION" e "ROLLBACK TRANSACTION" é considerado uma transação. Uma transação pode ser CONFIRMADA(Commit) ou CANCELADA(RollBack).

Quando o banco de dados encontra o termo "BEGIN TRANSACTION" (ou algo parecido, dependendo do banco de dados), começa a executar tudo em memória, depois, quando ele encontra "COMMIT TRANSACTION" ele literamente altera o banco de dados.
Caso ele encontre um "ROLLBACK TRANSACTION", ele cancela as atualizações feitas no banco.

Se ainda restam dúvidas sobre isso, aconselho que execute este comando acima no SQL Server. Será fácil ver o comportamento.

Agora, para combater o problema da concorrência de dados, existem os níveis de isolamento que toda transação deve ser submetida.
O isolamento determina o "escopo" em que a respectiva transação atuará, bem como o tipo de "travamento/lock" nos recursos do banco de dados. Os seguintes níveis de isolamento podem ser aplicados. (Todos níveis são aceitos pelo SQL Server, com exceção do "Chaos").

A. Chaos
As alterações ainda não confirmadas, das transações que possuem o nível de isolamento mais afinado não podem sobrescritas.

B. Read Uncommited
É capaz de previnir dados corrompidos de serem lidos. Os 3 problemas de concorrência de dados podem ocorrer ainda.

C. Read Commited (Padrão, quando o isolamento não é especificado)
Só será possível ler os dados que foram confirmados/commited. Com isto, é possível prevenir o "Dirty-Read".

D. Repeatable Read
Todos os dados utilizados pela transação são reservados/locked contra alterações. Desta maneira, não é possível acontecer "Dirty-Reads" ou "Repeatable-Reads". Entretanto, os dados utilizados podem ser excluidos externamento, portanto, "Phantom-Reads" pode acontecer.

E. Serializable
As transações são completamente isoladas uma da outra. Este tipo de isolamento previne os problemas citados acima.

F. Unspecified
Este tipo de isolamento ocorre quando existe um certo nível de isolamento, mas não é igual a nenhum dos acima.

2 - Aplicabilidade no Microsoft .NET Framework

Para utilização de transações no .NET Framework, será necessário trabalhar basicamente com 3 classes diretas.

  • SqlConnection (Implementa interface System.Data.IDbConnection)
  • SqlCommand (Implementa interface System.Data.IDbCommand)
  • SqlTransaction (Implementa interface System.Data.IDbTransaction)
Notem que coloquei as classes específicas para o provider do SQL Server como exemplo, mas as transações não estão restritas à este banco, e muito menos à este conjunto de classes. Basta procurar as classes que implementam as interfaces citadas acima que seu problema será resolvido.

2.1 - Papel do SqlConnection
Além de manter uma conexão com a fonte de dados, esta classe será a responsável pela criação semi-automática do objeto de transação, apartir do método .BeginTransaction(). Repare que este método tem 2 sobrecargas.

Na criação, você pode passar o nível de isolamento que deseja, de acordo com a enumeração System.Data.IsolationLevel (Chaos, ReadCommited, ReadUncommited, RepeatableRead, Serializable, Unspecified). Se nada for especificado, o isolamento ReadCommited será utilizado.
...
objConexao.Open()
Dim objTransacao as SqlTransaction = objConexao.BeginTransaction()
...

Não é necessário passar o comando SQL "BEGIN TRANSACTION" para o banco de dados, pois o próprio .NET Framework o fará.


fig. 2.a.: Detalhes da interface IDbConnection.

2.2 - Papel do SqlCommand
O SqlCommand é o responsável por enviar comandos SQL à fonte de dados. Foi criada a propriedade Transaction para que você associe o objeto SqlTransaction criado à ela. Assim, o .NET Framework saberá que todos os comandos enviados, devem ser encapsulados dentro da determinada transação.
...
objComando.Connection = objConexao
objComando.Transaction = objTransacao
objComando.CommandText = "...."
...

Basta preencher a propriedade Transaction e depois trabalhar normalmente com o SqlCommand.


fig. 2.b.: Detalhes da interface IDbCommand.

2.3 - Papel do SqlTransaction
Esta classe ficou muito simples de se trabalhar, 4 membros apenas.
Você deve chamar o método Commit() se desejar confirmar a transação e atualizar permanentemente a fonte de dados. Se quiser cancelar a transação, basta chamar o método Rollback().

Geralmente, usa-se com blocos Try/Catch.
...
Try
...
objComando.ExecuteNonQuery()
objTransacao.Commit()
Catch ()
objTransacao.Rollback()
...
Finally
objConexao.Close()
...
End Try
...
Se você criou o objeto SqlTransaction, apartir do SqlConnection.BeginTransaction(), a propriedade Connection já será preenchida automaticamente.
Você deve alterar a propriedade IsolationLevel de acordo com sua necessidade.


fig. 2.c.: Detalhes da interface IDbTransaction.

2.4 - SavePoints de Transações (.Save(""))
De acordo com a interface IDbTransaction, nós podemos chamar o método Rollback() e tudo que a transação tiver feito, será desfeito, correto? Bom, na classe específica SqlTransaction (para o SQL Server 7.0 ou posterior) é possível demarcar a transação com vários SavePoints(estágios), para que você possa efetuar um Rollback somente até determinado ponto da transação.
A figura 2.d. demonstra isso.


fig. 2.d.: Funcionamento dos SavePoints da classe SqlTransaction.
Para criar SavePoints, você deve chamar o método SqlTransaction.Save(ByVal savePointName As String) e dar um nome para ele, via parâmetro.
Depois, basta chamar o método SqlTransaction.Rollback(ByVal transactionName As String) passando o nome do SavePoint como parâmetro.

3 - Fechamento

Como visto, não é complicado trabalhar com transações no .NET Framework. Entretanto, é necessário saber escolher o momento apropriado para utiliza-las, pois são recursos que geram muito over-head no servidor.

Se quiser aprofundar no assunto, aconselho procurar pelos tópicos "Transações aninhadas", "Transações no MSDTC" e "Transações em cenário desconectado".

Envie seus comentários.

Marco Aurélio Péres

Marco Aurélio Péres - Blog: http://br.thespoke.net/MyBlog/Marco/MyBlog.aspx