Olá pessoal! Nesse post vou falar sobre o objetivo 1.3 do exame de certificação Microsoft 40-487, Implement transactions. Para acompanhar desde o começo e ver os primeiros posts, clique aqui.
Para o exame, é necessário que você saiba características gerais de transações e como implementá-las com ADO.NET e Entity Framework. Vamos lá?
Entendendo Transações
Basicamente, transações servem para fazer os dados do banco de dados serem consistentes. Elas são necessárias em caso de falha no meio de uma execução de comandos, servindo para que todos eles sejam revertidos. Por exemplo, ao tentar fazer a inserção de uma Account e depois uma AccountDetails, as duas inserções devem ser feitas com sucesso para que não haja um registro de AccountDetails sem Account e vice-versa. Entender esse conceito é fundamental para esse objetivo.
Outro aspecto importante de transações é se elas são simples ou distribuídas. Essencialmente, uma transação simples envolve apenas um banco de dados, enquanto uma distribuída envolve diversos bancos de dados, provavelmente de aplicações diferentes. Uma limitação da classe System.Data.SqlClient.SqlTransaction é que ela só trabalha com bancos de dados SQL Server.
Há também o conceito de nível de isolamento (isolation level) de uma transação. O enum IsolationLevel serve para controlar o comportamento de lock na execução de um comando. Esse enum existe em dois namespaces (System.Data e System.Transaction), mas ambos fazem a mesma coisa e têm os mesmos valores. A diferença apenas é que eles trabalham com as classes dos seus respectivos namespaces. O IsolationLevel pode ser mudado durante a execução da transação sem problemas. Vamos ao valores e comportamento deles:
- Unspecified: Não determinado. O nível de isolamento será determinado pelo driver do banco de dados.
- Chaos: As alterações pendentes de transações mais altamente isoladas não podem ser substituídas. Esse valor não é suportado no SQL Server ou Oracle, por isso tem uma utilização bem limitada.
- ReadUncommitted: Nenhum shared lock é emitido. Isso implica que a leitura pode ser imprecisa, o que geralmente não é desejado.
- ReadCommitted: Emite shared locks nas leituras. Isso evita leituras imprecisas, mas os dados ainda podem ser mudados antes do fim da execução da transação. Pode resultar em nonrepeatable reads ou phantom data.
- RepeatableRead: Locks são colocados em todos os dados usados na query, o que previne quaisquer alterações durante a execução da transação. Isso acaba com o problema de nonrepeatable read, mas phantom data ainda pode ocorrer.
- Serializable: Um range lock é colocado em um específico DataSet, o que previne quaisquer inserções ou alterações até que a transação termine. Deve ser usado rapidamente.
- Snapshot: Uma cópia dos dados é feita, então uma execução de transação pode ler enquanto outra pode modificar os mesmos dados. Não é possível ver os dados de outras execuções de transações, mesmo rodando a query novamente. O tamanho disso pode também causar problemas se não for usado rapidamente.
Implementando Transações com TransactionScope (System.Transactions)
TransactionScope é simples e poderoso. Vamos ao código e algumas características importantes:
- O método Complete() faz com que todas as alterações sejam comitadas. Se não for chamado, todas elas são desfeitas.
- Caso alguma Exception seja lançada dentro do bloco using, todas as alterações serão desfeitas também.
- A transação é promovida de simples para distribuída automaticamente quando a segunda SqlConnection é aberta.
- Mesmo numa transação distribuída, há algumas limitações óbvias. Cópias de arquivos e chamadas para web services, por exemplo, não são revertidas (alguns web services podem até suportar transações, mas isso não acontece automaticamente). Há também o requerimento de que o banco de dados suporte transações, o que não acontece com todos.
Implementando Transações com EntityTransaction
Você pode implementar transações com a classe EntityTransaction para as classes EntityCommand e EntityConnection. Ela tem duas principais propriedades: Connection e IsolationLevel. Tem também dois métodos importantes e óbvios: Commit() e Rollback().
Um ponto não óbvio e importantíssimo é que, usando DbContext ou ObjectContext (explicados aqui), não é necessário usar a classe EntityTransaction. Em qualquer DbContext.SaveChanges(), por exemplo, o Entity Framework usa uma transação para salvar todas as modificações feitas. Caso aconteça algum erro, uma Exception é lançada e todas as alterações são desfeitas.
Caso queira usar a classe EntityTransaction para explicitamente fazer uma transação, o caminho é bem simples. Basta criar uma EntityCoonnection, declarar uma Entity Transaction chamando BeginTransaction() e, no final, chamar o método Commit() ou Rollback(). Segue o código:
Implementando Transações com SqlTransaction
O comportamento de uma SqlTransaction é quase idêntico ao de uma EntityTransaction. As mudanças estão nos nomes dos objetos e seus tipos.
Você deve criar uma SqlConnection, chamar o método BeginTransaction() passando um IsolationLevel e criar um ou mais SqlCommands passando como parâmetro o CommandText, a SqlConnection e a SqlTransaction. Ao invés de um CommandText, também é possível passar o nome de uma Stored Procedure e mudar o tipo do CommandType para StoredProcedure.
Depois de toda a lógica feita, basta chamar o método Commit() ou Rollback().
Uma última dica para o exame: se você for perguntado qual é o melhor cenário para aplicar uma transação entre uma query longa que envolve várias tabelas e uma query rápida que envolve uma tabela, escolha a última opção. Embora aparentemente uma query que envolve várias tabelas pareça o ideal, o livro alega que a duração e complexidade geraria colocaria problemas de lock e contenção. Então tá, né?
O próximo post será sobre o objetivo 1.4, Implement data storage in Windows Azure.
Até lá!