Transações com Hibernatepor: Raphaela Galhardo
(raphaela@jeebrasil.com.br) Nste artigo serão apresentados alguns conceitos relacionados a transações, bem como as mesmas são implementadas com o framework Hibernate. IntroduçãoUma transação é uma unidade de execução indivisível (ou atômica). Isso significa dizer que todas as etapas pertencentes a uma transação são completamente finalizadas ou nenhuma delas termina. Para exemplificar o conceito de transações, considere um exemplo clássico de transferência entre contas bancárias: transferir R$ 150,00 da conta corrente do cliente A para a conta corrente do cliente B. Basicamente, as operações que compõem a transação são:
Para a efetivação da transação descrita acima, seria necessário seguir os seguintes passos:
Caso ocorra algum problema (por exemplo: falta de energia, falha no computador, falha no programa, etc.), a execução dos passos anteriores pode ser interrompida. Se, por exemplo, houvesse interrupção logo após a execução do 3º passo, a conta A teria um débito de R$ 150,00 e ainda não teria sido creditado R$ 150,00 na conta corrente B. Neste caso, o banco de dados estaria em um estado inconsistente, afinal, R$ 150,00 teriam “sumido” da conta A sem destino. Dessa maneira, é de suma importância garantir que esses seis passos sejam totalmente executados. Caso haja alguma falha antes da conclusão do último passo, deve-se garantir também que os passos já executados serão desfeitos, de forma que ou todos os passos são executados ou todos os passos não são executados. Para garantir a consistência do banco, esses seis passos devem ser executados dentro de uma transação, já que ela é uma unidade de execução atômica. Resumindo, uma transação garante que a seqüência de operações dentro da mesma seja executada de forma única, ou seja, na ocorrência de erro em alguma das operações dentro da transação todas as operações realizadas desde o início podem ser revertidas e as alterações no banco de dados desfeitas, garantindo assim, a unicidade do processo. A transação pode ter dois fins: commit ou rollback. Quando a transação sofre commit, todas as modificações nos dados realizadas pelas operações presentes na transação são salvas. Quando a transação sofre rollback, todas as modificações nos dados realizadas pelas operações presentes na transação são desfeitas. Para que um banco de dados garanta a integridade dos seus dados deve possuir quatro características, conhecidas como ACID:
Modelos de TransaçõesAs definições do início de uma transação, de seu fim e das ações que devem ser tomadas na ocorrência de falhas são feitas através de um modelo de transação. Existem diversos modelos encontrados na literatura. Nesta seção serão abordados apenas quatro: Flat Transactions, Nested Transactions, Chained Transactions e Join Transactions.
Transações e Banco de DadosUma transação de banco de dados é formada por um conjunto de operações que manipulam os dados. A atomicidade de uma transação é garantida por duas operações: commit e rollback. Os limites das operações de uma transação devem ser demarcados. Assim, é possível saber a partir de qual operação a transação é iniciada e em qual operação ela finalizada. Ao final da execução da última operação que pertence à transação, todas as alterações no banco de dados realizadas pelas operações que compõe a transação devem ser confirmadas, ou seja, um commit é realizado. Se houver algum erro durante a execução de algumas das suas operações, todas as operações da transação que já foram executadas devem ser desfeitas, ou seja, um rollback é realizado. A Figura 1 ilustra esses conceitos.
Figura 1 - Estados do sistema durante uma transação Ambientes Gerenciados e Não GerenciadosAs seções seguintes referem-se às definições dos conceitos relacionados a transações JDBC e JTA, onde aparecem os termos ambientes gerenciados e não gerenciados. Esta seção destina-se a explicar sucintamente o que são esses termos. Os ambientes gerenciados são aqueles caracterizados pela gerência automática de transações realizadas por algum container. Exemplos de ambientes gerenciados são componentes EJB (Enteprise JavaBeans) executando em servidores de aplicações (JBoss, Geronimo, etc). Já os ambientes não gerenciados são cenários onde não há nenhuma gerência de transação, como por exemplo: Servlets, aplicações desktop, etc.. Transações JDBCA tecnologia JDBC (Java Database Connectivity) é um conjunto de classes e interfaces escritas em Java, ou API, que realiza o envio de instruções SQL (Structured Query Language) para qualquer banco de dados relacional. Uma transação JDBC é controlada pelo gerenciador de transações SGBD e geralmente é utilizada por ambientes não gerenciados. Utilizando um driver JDBC, o início de uma transação é feito implicitamente pelo mesmo. Embora alguns bancos de dados necessitem invocar uma sentença "begin transaction" explicitamente, com a API JDBC não é preciso fazer isso. Uma transação é finalizada após a chamada do método commit(). Caso algo aconteça de errado, para desfazer o que foi feito dentro de uma transação, basta chamar o método rollback(. Ambos, commit() e rollback(), são invocados a partir da conexão JDBC. A conexão JDBC possui um atributo auto-commit que especifica quando a transação será finalizada. Se este atributo for definido como true, ou seja, se na conexão JDBC for invocado setAutoCommit(true), ativa-se o modo de auto commit. O modo auto commit significa que para cada instrução SQL uma nova transação é criada e o commit é realizado imediatamente após a execução e finalização da mesma, não havendo a necessidade de após cada transação invocar explicitamente o método commit(). Em alguns casos, uma transação pode envolver o armazenamento de dados em vários bancos de dados. Nessas situações, o uso apenas do JDBC pode não garantir a atomicidade. Dessa maneira, é necessário um gerenciador de transações com suporte a transações distribuídas. A comunicação com esse gerenciador de transações é feita usando JTA (Java Transaction API). Transações JTAAs transações JTA são usadas em um ambiente gerenciável, onde existem transações CMT (Container Managed Transactions). Neste tipo de transação não há a necessidade de programação explícita das delimitações das transações, esta tarefa é realizada automaticamente pelo próprio container. Para isso, é necessário informar nos descritores dos EJBs a necessidade de suporte transacional às operações e como ele deve gerenciá-lo. O gerenciamento de transações é feito pelo Hibernate a partir da interface Transaction. API para Transações do HibernateA interface Transaction fornece métodos para a declaração dos limites de uma transação. A Listagem 1 apresenta um exemplo de uso de transações com a interface Transaction. A transação é iniciada a partir da invocação ao método session.beginTransaction(). No caso de um ambiente não gerenciado, uma transação JDBC na conexão JDBC é iniciada. Já no caso de um ambiente gerenciado, uma nova transação JTA é criada, caso não exista nenhuma já criada. Casa já existe uma transação JTA, essa nova transação une-se a existente. A chamada ao método tx.commit() faz com que os dados em memória sejam sincronizados com a base de dados. O Hibernate só realiza efetivamente o commit se o comando beginTransaction() iniciar uma nova transação (em ambos ambientes gerenciado ou não gerenciado). Se o beginTransaction() não iniciar uma nova transação (no caso de transações JTA isso é possível), então o estado em sessão é apenas sincronizado com o banco de dados e a finalização da transação é feita de acordo com a primeira parte do código fonte que a criou. Se ocorrer algum erro durante a execução do método acaoExecutada(), o método tx.rollback() é executado, desfazendo o que foi feito até o momento em que o erro ocorreu. Observa-se que no final do código a sessão é finalizada a partir do comando session.close(), liberando a conexão JDBC e devolvendo-a para o pool de conexões. Session session = sessions.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); acaoExecutada(); tx.commit(); } catch (Exception e) { if (tx != null) { try { tx.rollback(); } catch (HibernateException he) { //log he and rethrow e } } } finally { try { session.close(); } catch (HibernateException he) { throw he; } } Listagem 1 - Usando a Interface Transaction do Hibernate FlushingFlushing é o processo de sincronizar os dados em sessão (ou em memória) com o banco de dados. As mudanças nos objetos de domínio em memória feitas dentro do escopo de uma sessão (Session) não são imediatamente propagadas para o banco de dados. Isso permite ao Hibernate unir um conjunto de alterações e fazer um número mínimo de interações com o banco de dados, ajudando a minimizar a latência na rede. A operação de flushing ocorre apenas em três situações: quando é dado commit na transação, algumas vezes antes de uma consulta ser executada (em situações que alterações podem influenciar em seu resultado) e quando o método Session.flush() é invocado. O HIbernate possui um modo flush que pode ser definido a partir do comando session.setFlushMode(). Este modo pode assumir os seguintes valores:
Níveis de Isolamento de uma TransaçãoAs bases de dados tentam assegurar que uma transação ocorra de forma isolada, ou seja, mesmo que estejam acontecendo outras transações simultaneamente, é como se ela estivesse ocorrendo sozinha. O nível de isolamento de uma transação especifica que dados estão visíveis a uma sentença dentro de uma transação. Eles impactam diretamente no nível de acesso concorrente a um mesmo alvo no banco de dados por transações diferentes. Geralmente, o isolamento de transações é feito usando locking, que significa que uma transação pode bloquear temporariamente um dado para que outras transações não o acessem no momento que ela o está utilizando. Muitos bancos de dados implementam o nível de isolamento de uma transação através do modelo de controle concorrente multi-versões (MCC – Multiversion Concorrency Control). Dentre alguns fenômenos que podem ocorrer devido à quebra de isolamento de uma transação estão três:
Existem quatro níveis de isolamento da transação em SQL. Eles se diferenciam de acordo com a ocorrência ou não dos fenômenos anteriormente descritos, como mostrado na Tabela 1.
Tabela 1 - Níveis de Isolamento A escolha do nível de isolamento Read Uncommited não é recomendada para banco de dados relacionais, já que permite ler inconsistências e informações parciais (mudanças realizadas por uma transação ainda não finalizada podem ser lidas por outra transação). Se a primeira transação não for concluída, mudanças na base de dados realizadas pela segunda transação podem deixá-la com um estado inconsistente. Com o nível Read Commited, uma transação somente visualiza mudanças feitas por outras transações quando confirmadas, permitindo que transações só acessem estados consistentes do banco. No caso de uma atualização/exclusão de uma linha de alguma tabela por uma transação, pode ser que a mesma tenha acabado de ser modificada por uma transação concorrente. Nesta situação, a transação que pretende atualizar fica esperando a transação de atualização que iniciou primeiro ser efetivada ou desfeita. Se as atualizações da primeira transação forem desfeitas, seus efeitos serão desconsiderados e a segunda transação efetivará suas mudanças considerando a linha da tabela anteriormente lida. Caso contrário, a segunda transação irá ignorar a atualização caso a linha tenha sido excluída ou aplicará a sua atualização na versão atualizada da linha. O nível Repeatable Read não permite que uma transação sobrescreva os dados alterados por uma transação concorrente. Uma transação pode obter uma imagem completa da base de dados quando iniciada. Este nível é ideal para a geração de relatórios, pois em uma mesma transação, um registro é lido diversas vezes e seu valor se mantém o mesmo até que a própria transação altere seu valor. Em relação ao nível Serializable, ele fornece o nível de isolamento de transação mais rigoroso. Ele permite uma execução serial das transações, como se todas as transações fossem executadas uma atrás da outra. Dessa forma, pode-se perder um pouco da performance da aplicação. Configurando o nível de isolamentoNo Hibernate cada nível de isolamento é identificado por um número:
Para configurá-lo basta incluir a linha presente na Listagem 2 no arquivo de configuração *.cfg.xml. Neste exemplo, o nível de isolamento foi definido como Repeatable Read. hibernate.connection.isolation = 4 Listagem 2 - Configuração do Nível de Isolamento Referências
|