Usando transações em C# com SQL Server

Veja neste artigo como usar transações na execuções de instruções SQL no banco de dados SQL Server através de aplicações desenvolvidas na linguagem C#.

Introdução

O acesso a bancos de dados é uma necessidade comum à maioria das aplicações comerciais desenvolvidas nos últimos anos, afinal, essa é uma das principais “razões de ser” desses programas: armazenar os dados em uma base segura e de fácil manipulação.

Diariamente executamos dezenas ou centenas de scripts com as mais diversas finalidades, tais como consulta e atualização nos dados de um banco. Muitas vezes também é preciso executar vários scripts em sequência, de forma que um pode depender do anterior. Assim, se um falhar, os próximos não podem ser executados e os dados não podem sofrer alteração a menos que todos tenham obtido sucesso na execução.

Em situações como essa, a integridade dos dados é o fator de maior importância, por tanto, deve-se garantir que todas as regras predeterminadas para a execução dessas instruções sejam seguidas. Por exemplo, precisamos inserir valores em duas tabelas, mas isso só pode ser feito “aos pares”, ou seja, não devemos permitir a inserção em apenas uma tabela. Se a inserção em uma delas falhar, a outra não pode ser alterada.

Nesse contexto entra o conceito de transação, que podemos definir, a grosso modo, como o agrupamento de instruções (em bancos de dados) em um bloco lógico, de forma que o sucesso ou fracasso da execução desse bloco só podem ser totais. Ou seja, ou todas as instruções são executadas com sucesso, ou nada é persistido no banco.

Termos/comandos relacionados à transação

Quando trabalhamos com transações, existem basicamente três comandos fundamentais que são sempre utilizados:

Controlando transações no SQL Server com C#

Neste ponto entramos no principal objetivo do presente artigo, que é explicar como controlar transações durante a execução de instruções SQL no banco de dados SQL Server utilizando a linguagem C# e as classes do namespace System.Data.SqlClient.

Para os exemplos que serão apresentados, tomaremos o seguinte cenário: é preciso inserir registros em duas tabelas (Tabela1 e Tabela2), mas essas instruções só podem ser feitas juntas, se uma das duas falhar, a outra não deve ser efetivada.

Na Listagem 1 está o código que pode ser utilizado para criação de um database com as duas tabelas em questão.

Listagem 1: Script de criação do banco e tabelas

create database DBTeste go use DBTeste go create table Tabela1 ( CampoTexto varchar(10) ) go create table Tabela2 ( CampoNumerico int ) go

Utilizaremos então uma aplicação do tipo “Console Application” e trabalharemos apenas na classe principal (Main) da mesma. Assim, primeiramente vejamos o código onde as instruções de insert são executadas, sequencialmente, sem o uso de uma transação.

Listagem 2: Execução das instruções com sucesso sem transação

class Program { static void Main(string[] args) { SqlConnection con = new SqlConnection("String de Conexão com o DBTeste"); SqlCommand cmd1 = con.CreateCommand(); SqlCommand cmd2 = con.CreateCommand(); cmd1.CommandText = "insert into Tabela1 (CampoTexto) values ('UM')"; cmd2.CommandText = "insert into Tabela2 (CampoNumerico) values (2)"; con.Open(); try { cmd1.ExecuteNonQuery(); Console.WriteLine("Comando 1 executado com sucesso!"); cmd2.ExecuteNonQuery(); Console.WriteLine("Comando 2 executado com sucesso!"); } catch (SqlException ex) { Console.WriteLine(ex.Message); } finally { con.Close(); } Console.ReadLine(); } }

No código acima executamos os comandos da maneira mais tradicional, um após o outro e de forma independente. Executando a aplicação, temos as mensagens de sucesso apresentada no console e os dados persistidos corretamente no banco, como já se esperava.


Figura 1: Execução do primeiro exemplo


Figura 2: Dados persistidos após o primeiro exemplo

Nesse caso os dois comandos foram executados com sucesso, nenhuma exceção ocorreu. Vamos então simular um erro na execução do segundo comando. Como sabemos que a coluna CampoNumerico é do tipo int, podemos tentar inserir um valor inválido, por exemplo, um texto.

Tentaremos assim inserir o valor ‘DOIS’ na Tabela2, o que gerará um erro. Porém, o primeiro comando deve ser executado sem problemas e apenas a primeira tabela sofrerá alterações.

Observação: os dados inseridos após cada exemplo são excluídos, para facilitar a visualização do exemplo seguinte.

Listagem 3: Execução de comandos com erro sem transação

class Program { static void Main(string[] args) { SqlConnection con = new SqlConnection("String de Conexão com o DBTeste"); SqlCommand cmd1 = con.CreateCommand(); SqlCommand cmd2 = con.CreateCommand(); cmd1.CommandText = "insert into Tabela1 (CampoTexto) values ('UM')"; cmd2.CommandText = "insert into Tabela2 (CampoNumerico) values ('DOIS')"; con.Open(); try { cmd1.ExecuteNonQuery(); Console.WriteLine("Comando 1 executado com sucesso!"); cmd2.ExecuteNonQuery(); Console.WriteLine("Comando 2 executado com sucesso!"); } catch (SqlException ex) { Console.WriteLine(ex.Message); } finally { con.Close(); } Console.ReadLine(); } }

Executando o código, temos as seguintes mensagens no console:


Figura 3: Erro na execução do segundo comando

De fato houve um erro na execução do segundo comando (erro ao tentar converter o valor ‘DOIS’ para o tipo inteiro), porém, se listarmos os registros das duas tabelas, veremos que a primeira sofreu alterações, pois o registro foi inserido normalmente. Isso desrespeita a regra previamente definida, onde foi dito que o registros só poderiam ser inseridos aos pares.


Figura 4: Dados persistidos após o segundo exemplo

Para corrigir essa falha utilizaremos a classe SqlTransaction, que nos permite controlar a transação, efetuando o commit ou rollback nos casos de sucesso ou falha, respectivamente, de todas as instruções executadas.

Listagem 4: Executando comandos dentro de uma transação

class Program { static void Main(string[] args) { SqlConnection con = new SqlConnection("String de Conexão com o DBTeste"); SqlCommand cmd1 = con.CreateCommand(); SqlCommand cmd2 = con.CreateCommand(); cmd1.CommandText = "insert into Tabela1 (CampoTexto) values ('UM')"; cmd2.CommandText = "insert into Tabela2 (CampoNumerico) values ('DOIS')"; con.Open(); SqlTransaction tran = con.BeginTransaction(); try { cmd1.Transaction = tran; cmd1.ExecuteNonQuery(); Console.WriteLine("Comando 1 executado com sucesso!"); cmd2.Transaction = tran; cmd2.ExecuteNonQuery(); Console.WriteLine("Comando 2 executado com sucesso!"); tran.Commit(); } catch (SqlException ex) { tran.Rollback(); Console.WriteLine(ex.Message); } finally { con.Close(); } Console.ReadLine(); } }

Executando a aplicação, temos no console as mesmas mensagens exibidas no caso anterior, mas se verificarmos no banco, nenhum dado foi inserido em nenhuma das tabelas (Figura 4).


Figura 5: Dados persistidos após o terceiro exemplo

Basicamente foi feito o seguinte:

Observação: aqui não foram aplicados conceitos como refatoração, a fim de deixar o código mais enxuto. Pelo contrário, o código foi deixado da forma mais detalhada possível para facilitar a compreensão.

Conclusão

Vimos nesse artigo como funcionam as transações e o quão úteis podem ser na garantia da integridade das informações a serem gravadas em bancos de dados. Além dos principais conceitos a cerca de transações, vimos como utilizá-las em aplicações desenvolvidas na linguagem C#.

O mesmo pode ser feito utilizando a linguagem Visual Basic .NET, posto que os recursos aqui utilizados são do .NET Framework, sobre o qual atuam as lingugens C#, VB.NET, entre outras.

Até a próxima publicação.

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados