Hibernate: Framework de Mapeamento Objeto-Relacional

Hibernate: Framework de Mapeamento Objeto-Relacional

CENTRO PAULA SOUZA

FACULDADE DE TECNOLOGIA DE TAQUARITINGA

CURSO SUPERIOR DE TECNOLOGIA EM PROCESSAMENTO DE DADOS

HIBERNATE: FRAMEWORK DE MAPEAMENTO OBJETO-RELACIONAL

JAQUELINE MISTRON

ORIENTADOR: PROF. MS. MARCELO MARTINS LAFFRANCHI

Taquaritinga, SP

2009

JAQUELINE MISTRON

HIBERNATE: FRAMEWORK DE MAPEAMENTO OBJETO-RELACIONAL

Monografia apresentada à Faculdade de Tecnologia de Taquaritinga, como parte dos requisitos para a obtenção do título de Tecnólogo em Processamento de Dados.

Orientador: Prof. Ms. Marcelo Martins Laffranchi

Taquaritinga, SP

2009

FOLHA DE APROVAÇÃO

Jaqueline Mistron

Hibernate: Framework de Mapeamento Objeto-Relacional

Monografia apresentada à Faculdade de Tecnologia de Taquaritinga, como parte dos requisitos para a obtenção do título de Tecnólogo em Processamento de Dados.

Data da aprovação: ____/____/____

Banca Examinadora

______________________________________________________________________

Nome

______________________________________________________________________

Instituição

______________________________________________________________________

Assinatura

______________________________________________________________________

Nome

______________________________________________________________________

Instituição

______________________________________________________________________

Assinatura

______________________________________________________________________

Nome

______________________________________________________________________

Instituição

______________________________________________________________________

Assinatura

Dedico,

Aos meus familiares e amigos tão queridos,

aos professores que encontrei pela vida e compartilharam

seus conhecimentos com carinho e dedicação,

e aos meus pais por todo o amor e sabedoria, e por

me ensinarem a conquistar a verdadeira riqueza.

“A mente que se abre a uma nova ideia,

jamais retorna ao seu tamanho original.”

Albert Einstein

MISTRON, J. Hibernate: Framework de Mapeamento Objeto-Relacional. Taquaritinga/SP, 2009, 53 f. Faculdade de Tecnologia de Taquaritinga.

Resumo

Este trabalho tem como objetivo explicar as principais características do framework de mapeamento objeto-relacional Hibernate, entre elas: a arquitetura, as formas de mapeamento suportadas pelo framework, as formas de consulta que são possíveis no Hibernate, transações, concorrência e a técnica de cache que é utilizada para atingir um maior desempenho utilizando este framework. Porém, antes de tudo, neste trabalho serão esclarecidos pontos importantes, como o conceito de mapeamento objeto-relacional e os problemas existentes na comunicação entre o modelo de objetos e o modelo de dados de um banco de dados relacional. Dessa forma, o leitor poderá compreender, de forma geral, a importância do framework Hibernate no contexto atual de desenvolvimento de software e, também poderá enriquecer o conhecimento que possui, ou não, sobre este assunto. Todos os conceitos serão apresentados da forma mais simples possível, para que o trabalho não fique restrito apenas a comunidade de desenvolvedores, mas também a qualquer pessoa que se interesse pelo assunto.

Palavras-chave: Framework. Mapeamento. Software. Objeto.

MISTRON, J. Hibernate: Framework de Mapeamento Objeto-Relacional. Taquaritinga/SP, 2009, 53 f. Faculdade de Tecnologia de Taquaritinga.

ABSTRACT

This work aims to explain the main features of the framework of object-relational mapping Hibernate, including: architecture, ways of mapping supported by the framework, forms of consultation that are possible in Hibernate, transactions, competition and technical cache that is used to achieve higher performance using this framework. But before all this work will be informed of important points, as the concept of object-relational mapping and the problems in communication between the object model and data model of a relational database. So, the reader can understand, in general, the importance of Hibernate framework in the current context of software development and also enrich the knowledge you have, or not, on this subject. All concepts will be presented as simply as possible so that the work not restricted to the developer community, but also to anyone who is interested in this question.

Keywords: Framework. Mapping. Software. Object.

LISTA DE ILUSTRAÇÕES

Ilustração 1 – Mapeamento objeto-relacional 12

Ilustração 2 - Mapeamento objeto-relacional simplificado 16

Ilustração 3 - Arquitetura básica do MOR com Hibernate 20

Ilustração 4 - Abordagem simples da arquitetura do framework Hibernate 21

Ilustração 5 - Abordagem completa da arquitetura do framework Hibernate 22

Ilustração 6 - Ciclo de vida do objeto persistente 24

Ilustração 7 - Arquivo XML de mapeamento da classe persistente Aluno 26

Ilustração 8 - Código XML para mapear uma associação de muitos para um 27

Ilustração 9 - Código XML para mapear uma coleção de objetos 27

Ilustração 10 - Código XML mapeando uma associação de muitos para muitos 28

Ilustração 11 - Classe Aluno mapeada com Annotations 29

Ilustração 12 – Consulta HQL 30

Ilustração 13 – Consulta HQL com função de agregação 30

Ilustração 14 - Consulta HQL com Order By 31

Ilustração 15 - Consulta HQL com critérios 32

Ilustração 16 - Consulta com a interface Criteria 33

Ilustração 17 - Método que consulta a tabela Aluno utilizando Criteria 33

Ilustração 18 - Consulta com interface SQLQuery 34

Ilustração 19 - Estados do sistema durante uma transação 37

Ilustração 20 - Utilização da interface Transaction no Hibernate 39

Ilustração 21 - Transações concorrentes 41

Ilustração 22 - Locking Otimista: Atualização sobrescrita 42

Ilustração 23 - Locking Otimista: Abordagem com Version Number 43

Ilustração 24 - Primeira estratégia de lock otimista 44

Ilustração 25 - Segunda estratégia de lock otimista 44

Ilustração 26 – Terceira estratégia de lock otimista 45

Ilustração 27 - Transação sem lock 46

Ilustração 28 - Transação com lock 46

Ilustração 29 - Arquitetura de cache 49

LISTA DE SIGLAS

ACID – Atomicidade, Consistência, Isolamento e Durabilidade

API – Application Programming Interface

CMT - Container Managed Transactions

CORBA – Common Object Request Broker Architecture

DTD – Definição de Tipos de Documentos

EJB – Enterprise JavaBeans

HQL – Hibernate Query Language

JDBC – Java Database Connectivity

JTA – Java Transaction API

LGPL – Lesser General Public License

MOR – Mapeamento Objeto-Relacional

OO – Orientação a Objetos

ORM – Object Relational Mapping

SGBD – Sistema Gerenciador de Banco de Dados

SGBDR – Sistema Gerenciador de Banco de Dados Relacional

SQL – Structured Query Language

XML – Extensible Markup Language

SUMÁRIO

CENTRO PAULA SOUZA 1

JAQUELINE MISTRON 2

FOLHA DE APROVAÇÃO 1

Resumo 4

ABSTRACT 5

LISTA DE ILUSTRAÇÕES 6

LISTA DE SIGLAS 8

SUMÁRIO 9

INTRODUÇÃO 11

1 O Conceito de Mapeamento Objeto-Relacional (MOR) 12

1.1 Entendendo o descompasso dos paradigmas 13

1.2 Os Principais Tipos de Mapeamento Objeto-Relacional 15

1.3 Frameworks de MOR (Mapeamento Objeto-Relacional) 18

2 Hibernate 20

2.1 Arquitetura do framework Hibernate 20

2.2 Mapeamento Objeto-Relacional com Hibernate 24

2.2.1 Mapeamento de metadados com arquivo XML 25

2.2.2 Mapeamento com Hibernate Annotations 28

2.3 Tipos de Consulta 29

2.3.1 HQL: A linguagem de consultas do Hibernate 30

2.3.2 Consulta por critérios 31

2.3.3 Consulta com SQL nativo 34

2.4 Transações no Hibernate 34

2.4.1 Os modelos de transações 35

2.4.2 Transações e o Banco de Dados 36

2.4.3 Transações JDBC 37

2.4.4 Transações JTA 38

2.4.5 Interface para transações do Hibernate 38

2.4.6 Flushing 40

2.5 Concorrência no Hibernate 40

2.5.1 Lock otimista 41

2.5.2 Lock pessimista 45

2.6 Maior desempenho em aplicações com Hibernate 47

2.6.1 Caching 47

2.6.1.1 Arquitetura de cache com Hibernate 48

CONCLUSÃO 51

INTRODUÇÃO

Atualmente, utilizar linguagens de programação orientada a objetos e bancos de dados relacionais em projetos de software é uma constante. No entanto, os princípios básicos do paradigma da orientação a objetos e do modelo relacional são bastante diferentes. No modelo de objetos, os elementos (objetos) correspondem a abstrações de comportamento, já no modelo relacional, os elementos correspondem a dados no formato tabular. A fim de minimizar as dificuldades que as distinções entre o paradigma de orientação a objetos e o paradigma de modelo de dados relacional causam a um projeto quando utilizados em conjunto, os desenvolvedores optam também por incorporar ao projeto uma ferramenta capaz de intermediar a comunicação entre a aplicação OO e o banco de dados relacional. Tal ferramenta é mais conhecida como framework de mapeamento objeto-relacional. Entre algumas ferramentas utilizadas para realizar o mapeamento estão os frameworks iBatis, SQLObject, OJB (Object Relational Bridge) e, o mais famoso e utilizado, Hibernate e sua versão para a plataforma .Net da Microsoft, NHibernate. No decorrer deste trabalho, o framework Hibernate será apresentado ao leitor da forma mais clara e objetiva possível, mas antes disso, alguns conceitos relevantes no contexto de mapeamento objeto-relacional serão introduzidos a fim de deixar o entendimento sobre o framework Hibernate ainda mais preciso. Ao longo deste trabalho o termo MOR (ou ORM – Object Relational Mapping) será utilizado para designar a camada de persistência onde os comandos SQL são gerados automaticamente, a partir de uma descrição baseada em metadados (metadata) de mapeamento de objetos em relação às estruturas relacionais (tabelas).

1 O Conceito de Mapeamento Objeto-Relacional (MOR)

A persistência dos dados é um fator de grande importância em um projeto de software que trabalhe e dependa de informações, ou seja, que necessite de um mecanismo de armazenamento de dados. O MOR surgiu da necessidade de evitar o problema da comunicação entre o modelo de dados relacional e o modelo de objetos.

O mapeamento objeto-relacional tem como objetivo estabelecer uma comunicação entre a camada de dados, que é implementada por um SGBD relacional, e a camada da lógica de domínio, que por sua vez é implementada por uma linguagem OO.

De forma simples, pode-se entender que o mapeamento objeto-relacional é responsável por fazer os dados tabulares “conversarem” com as classes da aplicação orientada a objetos e, dessa forma, aperfeiçoar a manipulação dos dados nessas aplicações. A ilustração 1 exemplifica, de forma simples, o mapeamento objeto-relacional.

Ilustração 1 – Mapeamento objeto-relacional

Em outras palavras, segundo Coelho e Sartorelli (2004):

O mapeamento pode ser interpretado como um processo semelhante ao desenvolvimento de um compilador. A maioria dos compiladores de linguagens de programação converte um determinado código-fonte em um programa real através de três operações básicas: a análise léxica do código-fonte, que separa as palavras-chave e os tokens1 da linguagem de forma compreensível pelo compilador; em seguida, a análise sintática é efetuada, identificando construtores válidos de linguagem nos grupos de tokens; por fim, o gerador de código interpreta estes construtores e gera o código executável.

O processo de mapeamento de um objeto para uma tabela do banco de dados relacional, definido em uma estrutura de documento XML, segue o mesmo princípio. Primeiramente, analisa-se o código para reconhecer o que caracteriza um atributo e seus respectivos valores. Então, verifica-se se o documento é válido, ou seja, se o documento está bem formado e, também, se ele segue a estrutura definida em um esquema ou DTD (Document Type Definition – Definição de Tipos de Documentos) associado a ele. Depois destas análises, é possível extrair os dados do documento e determinar a melhor forma de adaptá-los em uma estrutura relacional.

O mapeamento realizado entre os objetos de uma linguagem orientada a objetos e as tabelas de um sistema gerenciador de banco de dados relacional possibilita a persistência de objetos de modo transparente, isto é, torna possível manipular os dados armazenados em um SGBDR diretamente pela linguagem Orientada a Objetos.

A seguir os dois paradigmas citados anteriormente serão definidos com maior clareza.

1.1 Entendendo o descompasso dos paradigmas

A maioria das aplicações desenvolvidas hoje em dia utiliza a tecnologia orientada a objetos, mas mantém como principal meio de persistência um banco de dados relacional (Coelho e Sartorelli, 2004, p. 18).

O paradigma da OO é baseado em princípios provados pela engenharia de software, enquanto o paradigma relacional é baseado em princípios matemáticos.

Por conta da distinção entre estes dois modelos existe uma dificuldade para implementar aplicações que envolvam uma camada de lógica de domínio programada em linguagem OO e uma camada de dados com informações armazenadas em um SGBD relacional. Este problema tornou-se tão comum que já foi batizado: impedance mismatch ou problema de impedância (Ambler, 2003).

A divisão de um software em camadas é um padrão de arquitetura de projetos muito utilizado no desenvolvimento de aplicações. Um software costuma ser dividido em três partes: a lógica de apresentação (ou interface com o usuário como é mais conhecida) diz respeito a como tratar a interação do usuário com o software, a lógica da camada de dados diz respeito à comunicação com outros sistemas como, por exemplo, um banco de dados responsável, antes de tudo, pelo armazenamento de dados persistentes e a lógica de domínio, também conhecida como lógica de negócio, diz respeito ao trabalho que a aplicação deve efetuar no domínio em que está trabalhando. Esta camada envolve cálculos baseados nas entradas e em dados armazenados, validação de dados provenientes da camada de apresentação e a compreensão exata de qual lógica de dados executar, levando em conta os comandos recebidos na apresentação.

Dessa forma, a camada de dados deve possuir uma fonte de dados que se comunique com as diversas partes da infraestrutura que uma aplicação requer para executar sua tarefa. É neste ponto que surge o problema da impedância entre o modelo de dados e o modelo de objetos, pois com a diferença entre as abordagens, o programador deverá programar em duas linguagens diferentes. A lógica de domínio da aplicação é implementada utilizando uma linguagem orientada a objetos, enquanto utiliza-se a SQL para criar e manipular dados no banco de dados. Quando os dados são recuperados do banco de dados relacional eles devem ser traduzidos para a representação específica da linguagem OO. Os bancos relacionais apresentam algumas limitações em termos de modelagem. Em SGBDR, não há o conceito de instância, comportamento e herança. Também há questões de desempenho, quando muitas junções são necessárias para representar um objeto, além do próprio descompasso semântico em relação às linguagens OO: há um conjunto limitado de tipos de dados no banco relacional, e um número infinito de classes em potencial na programação OO. Estes, entre outros fatores, tem como consequência o descompasso entre os modelos.

Apesar de tantos contratempos entre ambos os paradigmas, deixar de utilizar um ou outro num projeto de software é uma alternativa incogitável. Isso porque os bancos de dados relacionais constituem-se uma das formas mais utilizadas para resolver o problema de persistência dos dados. Eles são consagrados como abordagens extremamente flexíveis e robustas para o armazenamento de informações e, atualmente, a melhor forma para criar dados persistentes, atualizá-los e consultá-los.

Em um banco de dados relacional os dados e suas relações são organizados em tabelas, que formalmente são chamadas de relações. Este paradigma é muito simples e eficiente, por isso o sucesso desses bancos de dados em aplicações comerciais é enorme. Além disso, uma das maiores razões para o sucesso dos bancos de dados relacionais é a presença da SQL, que é a linguagem mais padronizada para comunicação com bancos de dados, além de possuir um núcleo de sintaxe comum e de fácil compreensão.

Contrário ao paradigma dos bancos de dados relacionais há o conceito de Orientação a Objetos, também amplamente difundido e aplicado nas linguagens de programação mais utilizadas atualmente. Este conceito pressupõe a organização de um software em termos de coleção de objetos com estrutura e comportamento próprios. Um objeto é uma entidade do mundo real que tem uma identidade, por exemplo, objetos podem representar entidades concretas (um arquivo no computador, uma motocicleta), ou entidades conceituais (um jogo, uma política de escalonamento em um sistema operacional). Além disso, os dados em uma abordagem OO são representados por objetos, incluindo os valores dos atributos e as operações do objeto. Há uma diferença de representação entre os dois paradigmas: os objetos são referenciados em memória, enquanto os dados são referenciados por chaves em tabelas, que existem fisicamente.

Portanto, as diferenças entre os dois modelos são inegáveis e os benefícios que ambos proporcionam à aplicação também. Assim, uma proposta de integração entre eles é o objetivo da técnica de MOR. Os mecanismos de MOR trabalham para transformar uma representação de dados em outra, através de técnicas de tradução entre os diferentes esquemas de dados, para, enfim, lidar com este descompasso em situações onde a utilização de um banco de dados relacional é imprescindível.

1.2 Os Principais Tipos de Mapeamento Objeto-Relacional

O mapeamento deve envolver cada elemento de um objeto, como seus atributos, relacionamentos e herança. Portanto, os conceitos da programação orientada a objetos devem ser mapeados para estruturas de tabelas relacionais.

A principal tarefa do mapeamento objeto-relacional é identificar as construções da orientação a objetos que se deseja extrair do modelo relacional. Pois, como as técnicas de MOR foram desenvolvidas para trabalhar como modelos entidade-relacionamento pré-existentes, a tarefa de tradução, ou mapeamento, consistirá na adaptação do modelo de objetos ao modelo de dados relacional.

As principais técnicas de mapeamento são:

  • Mapeamento classe-tabela: é o mapeamento de uma classe em uma ou mais tabelas, ou de uma tabela para uma ou mais classes e mapeamento de herança;

  • Mapeamento atributo-coluna: é o mapeamento de tipos em atributos;

  • Mapeamento relacionamento-chave estrangeira: consiste no mapeamento dos relacionamentos OO em relacionamentos entre tabelas, utilizando chaves estrangeiras.

A figura a seguir ilustra o MOR:

Ilustração 2 - Mapeamento objeto-relacional simplificado

Em modelos de dados muito simples, as classes podem ser mapeadas diretamente em tabelas, em uma relação um para um. Esta é a forma mais intuitiva de mapeamento classe-tabela, onde todos os atributos da classe persistente são representados por todas as colunas de uma tabela no modelo relacional. Dessa forma, cada instância do objeto pode ser armazenada em uma tupla (linha) da tabela. Mas, este tipo de modelo pode conflitar com o modelo entidade-relacionamento existente, que pressupõe a normalização das tabelas e a otimização de consultas.

Os atributos de uma classe podem ser mapeados para zero ou mais colunas de uma tabela de um SGBDR, pois os atributos de um objeto não são necessariamente persistentes. Caso um atributo seja um objeto por si só, o mapeamento pode ser feito para várias colunas da tabela. Os atributos podem ser definidos como:

  • Atributos primitivos: atributo de uma classe que é mapeado a uma coluna de uma tabela, ou seja, refere-se ao valor de um tipo de dados específico (int, float, double).

  • Atributos de referência: estes representam relacionamentos com outras classes, isto é, referem-se a atributos cujo tipo é uma referência a outro objeto ou conjunto de objetos (composição).

Em orientação a objetos, uma classe pode se relacionar com outra através de agregação ou associação. A cardinalidade (ou multiplicidade) de um relacionamento pode ser 1:1 (um para um), 1:N (um para muitos), N:1 (muitos para um) e N:M (muitos para muitos) e da mesma forma, utilizando as mesmas cardinalidades, as tabelas são relacionadas. Os relacionamentos, no modelo de objetos, são implementados com combinações de referências a objetos e operações.

Assim sendo, as relações entre objetos são implementadas de forma explícita por meio de atributos de referência, por outro lado, as relações entre tabelas são realizadas através de associações de chaves estrangeiras. Um framework de mapeamento objeto-relacional pode mapear as relações entre objetos utilizando as chaves estrangeiras das tabelas correspondentes.

Outra forma de relacionamento entre objetos são os relacionamentos recursivos (por exemplo, várias cidades podem ter um mesmo bairro). O mapeamento em tabelas pode ser feito da mesma forma que um mapeamento M:N, criando uma tabela associativa. Neste caso, uma coluna de cada classe associada será chave estrangeira da tabela associativa.

Sem dúvidas, um aspecto essencial da OO é a herança. Este conceito permite que dados e comportamentos de uma classe (classe pai ou superclasse) sejam reutilizados por subclasses (classes filhas), porém bancos de dados relacionais não possuem o conceito de herança, então entidades do modelo relacional não podem herdar atributos de outras entidades.

Os fatores citados acima não podem deixar de ser considerados no mapeamento e, além deles também é necessário citar a existência de uma restrição a respeito da ordem dos dados – o que envolve o mapeamento de coleções – e o mapeamento dos metadados, geralmente definidos em um documento XML, frequentemente empregado pelos frameworks MOR.

O mapeamento dos metadados nada mais é que a criação de um arquivo de detalhamento da forma como as colunas de um SGBDR serão mapeadas em atributos de objetos, incluindo informações a respeito da multiplicidade dos relacionamentos, junções, integridade referencial etc.

No processo de mapeamento também é importante ressaltar quais informações adicionais deverão ser mantidas pelo objeto como, por exemplo, informações de chave primária, contadores e números de versionamento.

Como foi visto, existe mais de uma forma de efetuar o mapeamento entre o modelo de objetos e o modelo de dados relacional. A estratégia de encapsulamento do acesso ao banco de dados é que determinará como será implementado o mapeamento.

1.3 Frameworks de MOR (Mapeamento Objeto-Relacional)

O conceito de reuso de código está sendo cada vez mais explorado pelos desenvolvedores de software para diminuir tempo e esforço de programação. Assim, os frameworks são desenvolvidos e pesquisados, permitindo que partes ou todo um sistema possa ser reutilizado.

Um framework pode ser definido como um conjunto de classes abstratas e concretas que provê uma infraestrutura genérica de soluções para um conjunto de problemas. Essas classes podem fazer parte de uma biblioteca de classes ou podem ser específicas da aplicação. Frameworks permitem reutilizar não só componentes isolados, como também toda a arquitetura de um domínio específico. Diferentes frameworks têm sido desenvolvidos nas últimas décadas, visando o reuso do software e consequentemente a melhoria da produtividade, qualidade e manutenibilidade.

Os frameworks de MOR são frameworks de infraestrutura que fornecem serviços de mapeamento objeto-relacional. De forma simplificada, é possível dizer que um framework de MOR é responsável pela transformação dos dados de um objeto em uma linha de uma tabela num banco de dados relacional, ou de forma inversa, com a transformação de uma linha da tabela em um objeto da aplicação.

Apesar de muitas linguagens apresentarem bibliotecas para realizar a tarefa da persistência dos objetos, como por exemplo, a API de serialização de objetos, este tipo de solução muitas vezes é inadequada para sistemas de maior complexidade, no qual se espera que o mecanismo de persistência seja consideravelmente mais robusto e poderoso.

Os frameworks de MOR trabalham independentemente da aplicação e do banco de dados. A camada de persistência deve intermediar a camada de dados e a camada de aplicação.

Esse tipo de solução tem como foco aplicações que necessitam acessar dados, bases heterogêneas, ou gerenciar objetos de negócio distribuídos e persistentes. Um framework de MOR deve apresentar recursos de consulta a dados, suporte a transações, concorrência, e enfim, possibilitar a persistência transparente, encapsulando o acesso ao banco de dados relacional.

São inúmeros os frameworks de MOR existentes hoje e, em maioria, são ferramentas públicas, mantidas por um ou mais desenvolvedores e disponibilizadas para que qualquer pessoa possa utilizá-las. Atualmente, as linguagens de programação orientadas a objeto mais utilizadas no desenvolvimento de aplicações já possuem uma implementação responsável pelo o MOR no projeto.

Um dos frameworks mais conhecido e utilizado pela comunidade de desenvolvedores é o Hibernate. Aliás, quando se fala em mapeamento objeto-relacional o Hibernate é, às vezes, o único framework lembrado por desenvolvedores. Devido a esse fato, o trabalho dará especial atenção a ele.

.

2 Hibernate

O Hibernate é um framework de mapeamento objeto-relacional desenvolvido na linguagem Java e é utilizado em aplicações também programadas nesta linguagem, mas também pode fazer parte de projetos desenvolvidos na plataforma .Net da Microsoft graças ao NHibernate.

O framework Hibernate, assim como a maioria dos frameworks MOR, é um software livre de código aberto distribuído com a licença LGPL (Lesser General Public License). Esta ferramenta faz o mapeamento entre o modelo de objetos e o modelo de dados relacional mediante a utilização de arquivos XML para estabelecer a relação.

Liderado por Gavin King, o projeto do Hibernate foi desenvolvido por vários desenvolvedores Java espalhados pelo mundo e, posteriormente, a empresa JBoss Inc (comprada pela Red Hat) contratou os principais desenvolvedores do projeto para fazer seu suporte.

2.1 Arquitetura do framework Hibernate

A arquitetura do Hibernate pode ser representada de forma geral pela ilustração abaixo:

Ilustração 3 - Arquitetura básica do MOR com Hibernate

O diagrama mostra o Hibernate utilizando o banco de dados e a configuração de dados para fornecer persistência de serviços (e de objetos) para a aplicação.

Não é possível ter uma visão mais detalhada da arquitetura em execução, pois o Hibernate é muito flexível e pode suportar várias abordagens, mas existe a possibilidade de mostrar os dois extremos em diagramas. A seguir, a ilustração exibe a arquitetura mais simples onde o aplicativo fornece suas próprias conexões JDBC2 e gerencia suas transações. Nesta abordagem, o mínimo de subconjuntos das APIs do Hibernate são utilizados:

Ilustração 4 - Abordagem simples da arquitetura do framework Hibernate

Por outro lado, na abordagem “completa” a aplicação não tem que lidar diretamente com JDBC/JTA e APIs, o Hibernate se encarrega de todos os detalhes, como é possível observar a seguir:

Ilustração 5 - Abordagem completa da arquitetura do framework Hibernate

Os objetos do diagrama mostrado na ilustração acima são definidos a seguir:

  • SessionFactory: este objeto é aquele que mantém o mapeamento objeto-relacional em memória. Trabalha como uma fábrica de objetos Session, a partir dos quais os dados são acessados. Um objeto SessionFactory é threadsafe3, porém deve existir apenas uma instância dele na aplicação, pois é um objeto muito pesado para ser criado várias vezes. No caso de o aplicativo precisar efetuar acesso a múltiplas bases de dados usando Hibernate, o desenvolvedor precisará criar uma SessionFactory para cada banco.

  • Session: este objeto possibilita a comunicação entre a aplicação e a persistência, através de uma conexão JDBC. É um objeto leve de ser criado, não deve ter tempo de vida por toda a aplicação e não é threadsafe. Um objeto Session possui um cache4 local

de objetos recuperados na sessão. Com ele é possível criar, remover, atualizar e consultar objetos persistentes.

  • Objetos persistentes: são objetos de vida curta que possuem estado persistente, ou seja, continuam existindo mesmo após o encerramento da Session em que estão associados. Quando a Session é fechada, eles são separados e liberados para serem usados dentro de qualquer camada de aplicação.

  • Objetos transientes: são instâncias de classes persistentes que ainda não estão associadas a uma Session. Um objeto transiente pode ter sido instanciado pela aplicação e não persistido ainda, ou ele foi instanciado por uma Session que foi encerrada.

  • Transaction: esta interface (objeto) é utilizada para representar uma unidade indivisível de uma operação de manipulação de dados. O uso dessa interface em aplicações que usam Hibernate é opcional. Ela abstrai a aplicação dos detalhes das transações JDBC, JTA ou CORBA.

  • ConnectionProvider (Configuration): O ConnectionProvider também é opcional. Trata-se de uma fábrica (e combinações) de conexões JDBC. Este objeto abstrai a aplicação de lidar diretamente com Datasource (armazena informações sobre a conexão com a base de dados) ou DriverManager (gerenciador de drivers). Este objeto não é exposto para a aplicação, mas o programador pode implementá-lo ou estendê-lo.

  • TransactionFactory: outro objeto opcional. Entendido como uma fábrica para instâncias de Transaction. Não é exposta à aplicação, mas como o ConnectionProvider pode ser estendido ou implementado pelo programador.

Dada uma arquitetura simples, a aplicação passa pelas APIs Transaction, TransactionFactory e/ou ConnectionProvider para poder se comunicar diretamente com a transação JTA ou JDBC.

Nesta parte do trabalho também é importante explicar os possíveis estados de um objeto.

Uma instância de uma classe persistente pode estar em um dos três diferentes estados, que são definidos respeitando um contexto persistente. O objeto Session do Hibernate é o contexto persistente e o estado dos objetos pode ser: transiente, persistente ou detached.

Quando um objeto é transiente a instância não é e nunca foi associada com nenhum contexto persistente, isto é, não possui uma identidade persistente (valor da primary key). Por outro lado, quando o objeto é persistente a instância está atualmente associada a um contexto persistente, isto é, possui uma identidade persistente (valor da primary key) e, talvez, correspondente a um registro no banco de dados. Além dos estados transiente e persistente, um objeto poder ser detached (desatachado). Está neste estado quando a instância foi associada com um contexto persistente, porém este contexto foi fechado, ou a instância foi serializada por outro processo. O objeto desatachado possui uma identidade persistente e, talvez, corresponda a um registro no banco de dados. Os estados dos objetos podem ser facilmente explicados com a ilustração seguinte.

Ilustração 6 - Ciclo de vida do objeto persistente

De acordo com a ilustração acima, inicialmente, o objeto pode ser criado e ter o estado transiente ou persistente. Um objeto em estado transiente se torna persistente se for criado ou atualizado no banco de dados. Já um objeto em estado persistente, pode retornar ao estado transiente se for apagado do banco. Também pode passar ao estado detached se, por exemplo, a sessão (Session) com o banco de dados for fechada. Um objeto no estado detached pode voltar ao estado persistente se, por exemplo, for atualizado no banco de dados. Tanto do estado detached quanto do estado transiente o objeto pode ser coletado para destruição.

Com base nos diagramas e conceitos explicados acima, é possível compreender melhor o funcionamento do framework Hibernate e, desta forma, usufruir da ferramenta de forma sábia, tirando proveito de todas as suas funcionalidades.

2.2 Mapeamento Objeto-Relacional com Hibernate

O framework Hibernate, assim como as demais ferramentas de MOR, requer metadados para determinar como a comunicação entre classes e tabelas do banco relacional será realizada.

O meio mais comum para gerar metadados é através da definição de um esquema em um arquivo XML. Mas esta não é a única forma de realizar o mapeamento, pois a partir da plataforma Java 5.0 foi incorporado um recurso chamado Annotations e com ele o programador pode dispensar a criação de um arquivo XML para cada classe que pretender transformar em tabela, já que pode incluir as Annotations no próprio código da classe Java e assim obter o mapeamento entre elas e as tabelas do banco que serão criadas automaticamente.

Dois tipos de anotações são suportados pelo Java 5.0: anotações simples e meta anotações. As anotações simples são usadas apenas para agregar significado especial ao código fonte, mas não para criar algum tipo de anotação. Já as meta anotações são aquelas utilizadas para definir tipos de anotações.

As duas formas de mapeamento serão apresentadas a seguir neste trabalho.

2.2.1 Mapeamento de metadados com arquivo XML

O mapeamento objeto-relacional é normalmente definido em um documento XML, que é projetado para ser legível e editável a mão. A linguagem de mapeamento é centrada em Java, o que significa que os mapeamentos são construídos baseados em declarações de classes persistentes e não em declarações de tabelas.

Um exemplo de arquivo XML responsável pelo mapeamento da classe persistente “Aluno” pode ser visto na ilustração 7.

Ilustração 7 - Arquivo XML de mapeamento da classe persistente Aluno

No código da ilustração acima existe um bom exemplo de associação muitos para um, o mapeamento de uma coleção com associação um para muitos e de uma associação muitos para muitos.

A associação muitos para um é realizada entre as classes Aluno e Cidade, na qual um aluno matriculado em uma determinada escola tem em seu registro uma cidade associada a ele e, essa mesma cidade pode ser associada a outros alunos. Portanto, um aluno reside em uma cidade e uma cidade pode ter um ou vários alunos residindo nela. A ilustração abaixo destaca este relacionamento.

Ilustração 8 - Código XML para mapear uma associação de muitos para um

O mapeamento de coleção é realizado entre as classes Aluno e Documento, no qual um aluno é matriculado em uma determinada escola com uma lista de documentos associada a ele e, uma lista só diz respeito a um aluno. Portanto, um aluno tem vários documentos. O Hibernate tem vários elementos para realizar o mapeamento de coleções, como <list>, <set>, <map> e <bag> e a escolha de um ou outro depende do tipo de interface. Neste caso, entre Aluno e Documento, o elemento utilizado foi <bag>.

Ilustração 9 - Código XML para mapear uma coleção de objetos

A associação muitos para muitos é realizada entre as classes Aluno e Disciplina, onde um aluno está matriculado em várias disciplinas e, uma disciplina pode ter vários alunos matriculados. Este relacionamento gera uma terceira tabela (tabela associativa) no banco de dados, AlunoDisciplina, que conterá duas referências de chave-estrangeira, uma para Aluno e outra para a Disciplina. O elemento utilizado para realizar este mapeamento é o <idbag>.

Ilustração 10 - Código XML mapeando uma associação de muitos para muitos

2.2.2 Mapeamento com Hibernate Annotations

Hibernate Annotations é um pacote de suporte ao mapeamento de metadados utilizando anotações. Ele está disponível para download separadamente e, depois de realizado pode ser configurado facilmente para ser utilizado em projetos. As anotações podem ser definidas como metadados que aparecem no código fonte e são ignorados pelo compilador. Qualquer símbolo em um código Java que comece com uma @ (arroba) é uma anotação.

Utilizar as anotações pode agilizar o desenvolvimento do projeto, uma vez que o programador não precisará criar arquivos XML para mapear os metadados, pois isto será feito na própria classe persistente.

A classe Aluno pode ser mapeada com anotações como é possível observar na ilustração 11 que segue.

Ilustração 11 - Classe Aluno mapeada com Annotations

2.3 Tipos de Consulta

A razão de manter dados gravados em um banco de dados é poder consultá-los quando existir necessidade. Em projetos que utilizam o framework Hibernate existem três formas de realizar consultas. A primeira é utilizando a HQL (Hibernate Query Language), linguagem de consulta nativa do Hibernate, orientada a objetos e fácil, porém poderosa. Outro meio de recuperar os dados no banco é através da consulta por Critério, que utiliza a API Criteria. Por fim, a terceira forma de realizar consultas quando se utiliza o Hibernate é por meio de SQL nativa do banco de dados.

2.3.1 HQL: A linguagem de consultas do Hibernate

O Hibernate possui uma poderosa linguagem de consulta que foi desenvolvida, intencionalmente, com uma sintaxe muito parecida com a SQL, mas ao contrário da SQL, a HQL é totalmente orientada a objetos e requer conhecimentos de herança, polimorfismo e associações por parte do desenvolvedor.

As consultas realizadas com HQL não são case-sensitive, exceto pelos nomes das classes e propriedades escritas, por exemplo, na linguagem Java. Por exemplo, escrever uma consulta com um “sELEct” ou com “SELECT” é a mesma coisa, o resultado será o mesmo, mas utilizar “org.hibernate.exemplo.ALUNO” é diferente de “org.hibernate.exemplo.Aluno” e, isto causará erros na consulta caso o desenvolvedor não perceba o descuido que teve com a sintaxe do código.

O poder da linguagem de consultas é um dos pontos principais na distribuição do Hibernate. As próximas ilustrações deste trabalho darão exemplos da utilização da HQL nas consultas.

Ilustração 12 – Consulta HQL

A cláusula “select” seleciona quais objetos e propriedades devem retornar no resultado da query. No caso acima, o resultado da consulta serão os alunos, cujo nome contém a letra “A”.

Ilustração 13 – Consulta HQL com função de agregação

O select acima utiliza uma função de agregação e traz como resultado da consulta todos os alunos de uma determinada turma. As funções agregadas suportadas em queries HQL são:

  • avg (...), sum (...), min (...), max (...)

  • count (*)

  • count (…), count (distinct …), count (all …)

Além das funções de agregação, é possível utilizar operadores aritméticos, concatenação e funções SQL reconhecidas na cláusula “select”.

Ilustração 14 - Consulta HQL com Order By

A figura anterior mostra uma consulta que utiliza a cláusula “order by” para ordenar o resultado da consulta. As cláusulas “group by” e “having” também são suportadas pela HQL.

2.3.2 Consulta por critérios

O Hibernate oferece uma intuitiva e poderosa API de critério de consultas.

A interface Criteria representa a query ao invés de uma classe persistente particular. Então, a sessão (Session) é uma fábrica para instâncias de Criteria.

Esta API é uma ótima alternativa para desenvolvedores que não têm muita intimidade com a SQL e, sem dúvidas, pretendem deixar o código orientado a objetos livre de consultas utilizando SQL ou HQL seguindo as boas práticas da programação OO. Além disso, com esta técnica é possível aperfeiçoar as consultas, como é visto nas ilustrações seguintes.

Ilustração 15 - Consulta HQL com critérios

No trecho de código HQL destacado são realizadas as restrições da consulta. Estas mesmas restrições podem ser realizadas utilizando a API Criteria. Desta forma, a consulta ficaria apenas como mostra a próxima ilustração.

Ilustração 16 - Consulta com a interface Criteria

A próxima imagem também é um exemplo de como a utilização de criteria para realizar consultas pode deixar o código mais claro e fácil de entender.

Ilustração 17 - Método que consulta a tabela Aluno utilizando Criteria

O método da figura acima realiza uma busca aos objetos Aluno que estão gravados no banco de dados relacional a partir de um valor que é passado como parâmetro pelo usuário da aplicação. As restrições da busca exigem que o aluno encontrado na busca tenha um nome, ou matrícula igual a do aluno informado como parâmetro da consulta. O resultado desta consulta será exibido ordenado pelo nome do aluno de forma ascendente.

Como é possível notar, a API Criteria do Hibernate também fornece o mais variado tipo de cláusulas e expressões para consultar objetos persistidos.

2.3.3 Consulta com SQL nativo

Outra forma de criar consultas quando se utiliza o framework Hibernate é com o dialeto SQL nativo do banco de dados usado no projeto. Isto é útil quando se deseja aproveitar características específicas do SGBD ou, ainda, pode ser um caminho de migração simples de uma aplicação baseada em SQL/JDBC para uma aplicação Hibernate.

A execução de consultas SQL nativas é controlada pela interface SQLQuery, que também é uma API para consultas do Hibernate.

Ilustração 18 - Consulta com interface SQLQuery

Acima a ilustração exibe um simples “Select” utilizando SQL nativo responsável por recuperar na base de dados um aluno com todas as suas informações. Dentro do código da linguagem orientada a objetos é inserida a SQL e, novamente é necessário ressaltar que incluir SQL em um código orientado a objetos é, indubitavelmente, uma péssima prática na programação orientada a objetos.

2.4 Transações no Hibernate

Uma transação é uma unidade de execução indivisível (ou atômica), isto é, todas as etapas pertencentes a uma transação são completamente finalizadas ou nenhuma delas termina.

Em síntese:

Uma transação garante que a sequência de operações dentro dela seja executada de forma única, ou seja, caso ocorra erro em alguma das operações dentro da transação todas as operações executadas desde o início podem ser revertidas e as alterações no banco de dados desfeitas, garantindo dessa forma, a unicidade do processo. A transação pode ter dois fins: commit ou rollback. (Fernandes; Lima, 2007, p.77).

Quando uma 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 garantir a integridade dos seus dados, um banco de dados deve possuir quatro características conhecidas como ACID:

  • Atomicidade: o banco de dados deve garantir que todas as transações sejam indivisíveis.

  • Consistência: após a execução de uma transação, o banco de dados deve continuar consistente, ou seja, deve continuar com um estado válido.

  • Isolamento: mesmo que várias transações ocorram paralelamente (ou concorrentemente), nenhuma transação deve influenciar nas outras. Resultados parciais de uma transação não devem ser “vistos” por outras transações executadas concorrentemente.

  • Durabilidade: após a finalização de uma transação, todas as alterações realizadas por ela no banco de dados devem ser duráveis, mesmo havendo falhas no sistema após a sua finalização.

2.4.1 Os modelos de transações

Os detalhes do início de uma transação, de seu fim e das ações que devem ser tomadas caso ocorra falhas são definidas através de um modelo de transação.

Nesta seção serão abordados apenas quatro entre os muitos modelos existentes, são eles: Flat Transactions, Nested Transactions, Chained Transactions e Join Transactions.

O Flat Transactions é o modelo mais utilizado pela maioria dos sistemas gerenciadores de banco de dados (SGBD) e gerenciadores de transações. Ele é conhecido como modelo de transações planas, pois apresenta uma única camada de controle, ou seja, todas as operações dentro da transação são tratadas como uma única unidade de trabalho.

O modelo Nested Transaction também conhecido como Modelo de Transações Aninhadas, possibilita que uma transação possa ser formada por várias sub-transações. Em outras palavras, uma única transação pode ser dividida em diversas unidades de trabalho, com cada unidade operando independente das outras, ou seja, a propriedade de atomicidade é válida para as sub-transações. Além disso, uma transação não pode ser validada até que todas as suas sub-transações sejam finalizadas e se uma transação for interrompida, todas as suas sub-transações também serão. O contrário não é verdadeiro, já que se uma sub-transação for abortada a transação que a engloba pode: ignorar o erro, desfazer a sub-transação ou iniciar outra sub-transação.

O terceiro modelo de transação é o Chained Transaction que também é conhecido como Modelo de Transações Encadeadas. Esse modelo tem como objetivo desfazer as operações de uma transação em caso de erro com a menor perda de trabalho possível. Uma transação encadeada consiste em um conjunto de sub-transações executas sequencialmente, em que à medida que as sub-transações vão sendo executadas, são validadas e não podem mais ser desfeitas. Os resultados do conjunto de transações só serão visíveis ao final da execução de todas elas.

Por fim, o modelo Join Transaction que permite que duas transações sejam unidas em uma só, de forma que todos os recursos se tornam compartilhados.

2.4.2 Transações e o Banco de Dados

Uma 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. Dessa forma, é 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 ilustração 19 exibe esses conceitos.

Ilustração 19 - Estados do sistema durante uma transação

As próximas seções deste trabalho referem-se às definições dos conceitos relacionados a transações JDBC e JTA, onde aparecem os termos ambientes gerenciados e não gerenciados. Portanto, eles serão explicados nesta seção.

Os ambientes gerenciados são aqueles caracterizados pela gerência automática de transações realizadas por algum container. São exemplos de ambientes gerenciados os componentes EJB (Enterprise JavaBeans) executando em servidores de aplicações (JBoss, Tomcat Apache, Geronimo). 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.

2.4.3 Transações JDBC

A tecnologia JDBC (Java Database Connectivity) é um conjunto de classes e interfaces escritas na linguagem 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 do SGBD e geralmente é utilizada por ambientes não gerenciados. Utilizando um driver JDBC, o início de uma transação é realizado implicitamente pelo mesmo. Embora alguns bancos de dados necessitem invocar uma sentença “begin transaction” explicitamente, com a API JDBC isso não é necessário. Uma transação é finalizada após a chamada do método commit(). No caso de acontecer algo errado, para desfazer o que foi feito dentro de uma transação, basta invocar o método rollback(). Ambos os métodos são invocados a partir da conexão JDBC.

A conexão JDBC possui um atributo chamado “auto commit” que especifica quando a transação será finalizada. Se este atributo for definido com o valor booleano true, o modo de auto commit é ativado e, isto 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. Desta forma, não haverá necessidade de invocar explicitamente o método commit() após cada transação.

Em alguns casos, uma transação pode envolver o armazenamento de dados em bases distintas. Nessas situações, apenas o uso do JDBC não pode garantir a atomicidade. Assim, é necessário um gerenciador de transações com suporte a transações distribuídas. A comunicação com esse gerenciador de transações é realizada utilizando a interface JTA.

2.4.4 Transações JTA

As transações JTA (Java Transaction API) são usadas em um ambiente gerenciável, onde existem transações CMT (Container Managed Transactions). Neste tipo de transação não existe a necessidade de programar explicitamente as delimitações das transações, pois 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 é realizado pelo Hibernate a partir da interface Transaction.

2.4.5 Interface para transações do Hibernate

A API Transaction oferece métodos para declarar os limites de uma transação. A transação é iniciada a partir da invocação do método session.beginTransaction(). No caso de um ambiente não gerenciado, uma transação JDBC na conexão JDBC é iniciada. No entanto, se o ambiente for gerenciado, uma nova transação JTA é criada, caso ainda não exista nenhuma. Se já existir uma transação JTA, a que foi criada será unida a existente.

A chamada ao método commit() faz com que os dados em memória sejam sincronizados com o banco de dados. O Hibernate só realiza o commit efetivamente se o comando beginTransaction() iniciar uma nova transação (em ambos ambientes, gerenciado e não gerenciado). Se o beginTransaction() não iniciar uma nova transação, então o estado em sessão é apenas sincronizado com a base de dados e a finalização da transação é realizada de acordo com a primeira parte do código fonte que a criou.

Entretanto, se ocorrer algum erro durante a execução de uma operação na transação, o método rollback é executado e desfaz todas as ações realizadas até o momento do erro. A próxima ilustração é um trecho de código Java que serve para ilustrar as ideias apresentadas.

Ilustração 20 - Utilização da interface Transaction no Hibernate

É possível observar que no final do código, da ilustração acima, a sessão é finalizada com o comando session.close(). Dessa forma, a conexão JDBC é liberada e devolvida ao pool5 de conexões.

2.4.6 Flushing

Flushing é 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 realizadas 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 somente em três situações: quando é executado 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:

  • FlushMode.AUTO: é o valor padrão e faz com que o Hibernate não realize o processo de flushing antes de todas as consultas e, somente realizará se as mudanças dentro da transação alterarem seu resultado.

  • FlushMode.COMMIT: especifica que os estados dos objetos em memória somente serão sincronizados com a base de dados ao final da transação, ou seja, quando o método commit() for chamado.

  • FlushMode.NEVER: especifica que a sincronização só será realizada diante da chamada explícita ao método flush().

2.5 Concorrência no Hibernate

Em algumas situações pode acontecer de duas ou mais transações que ocorrem paralelamente lerem e atualizarem o mesmo dado. Levando em conta que duas transações leiam um mesmo dado, quase que simultaneamente, ambas as transações vão manipular esse mesmo dado com operações diferentes e atualizá-lo na base de dados. Para exemplificar, a próxima ilustração apresenta um exemplo de duas transações concorrentes manipulando o mesmo dado.

No primeiro passo, ambas as transações leem o dado X com o mesmo valor (2). Em seguida, T1 soma o valor X que leu com 1 e o valor de X para T1 passa a ser 3 (2 + 1). Por outro lado, T2 soma o valor lido de X a 3 e X passa a ter o valor 5 (2 + 3).

Por fim, ambos T1 e T2 gravarão os novos valores de X calculados na base de dados, respectivamente. Como não há controle de concorrência de acesso ao dado X, o seu valor final corresponderá a 5, ou seja, o valor calculado por T2, significando que as alterações feitas por T1 foram descartadas. Em outras palavras, a transação T2 sobrescreveu a T1.

Ilustração 21 - Transações concorrentes

Para evitar a situação descrita anteriormente, deve-se controlar o acesso concorrente ao dado, ou seja, deve-se implementar o mecanismo de Locking. O gerenciamento de locking e da concorrência pode ser feito de duas formas:

  • pessimista: utilizar o controle pessimista significa que se uma transação T1 lê um dado e tem a intenção de atualizá-lo, esse dado será bloqueado (nenhuma outra transação poderá lê-lo) até ser liberado por T1, normalmente após a sua atualização.

  • otimista: utilizar o controle otimista significa que se T1 lê e altera um dado ele não será bloqueado durante o intervalo entre a leitura e atualização. Caso outra transação T2 tenha lido esse mesmo dado antes de T1 atualizá-lo e tente alterá-lo em seguida na base de dados, um erro de violação de concorrência deve ser gerado.

2.5.1 Lock otimista

Para ilustrar o gerenciamento do tipo otimista, um exemplo é dado a partir das ilustrações seguintes. O problema é exibido na Ilustração 22, onde, inicialmente, duas transações (ilustradas por Thread 1 e Thread 2) tentam fazer acesso a um mesmo dado no banco (Select dado), uma seguida da outra. Em seguida, a primeira transação (Thread 1) atualiza este dado na base de dados e depois ele também é atualizado pela segunda transação (Thread 2). Nesta abordagem otimista, a atualização efetuada pela segunda transação sobrescreve a atualização realizada pela primeira, ou seja, a atualização da primeira transação é perdida.

Ilustração 22 - Locking Otimista: Atualização sobrescrita

Para resolver o problema observado anteriormente com a abordagem otimista, pode-se utilizar o conceito de Version Number, que é um padrão utilizado para versionar numericamente os dados de uma linha de uma tabela no banco de dados.

Por exemplo, na ilustração 21, também, inicialmente, duas transações (ilustradas por Aplicação 1 e Aplicação 2) fazem acesso a um mesmo dado do banco (Select dado), uma seguida da outra. Dessa forma, o mesmo dado nas duas transações é rotulado com a versão atual dele na base de dados, neste caso, Versão 1. Em seguida, a segunda transação atualiza o dado, mas no momento da atualização ser realizada, acontece uma verificação para certificar se a versão do dado na transação corresponde a versão dele no banco de dados. Nesta primeira atualização, a versão do dado na transação é 1 e no banco idem, portanto, a atualização é efetivada e a versão no banco passa a ser a Versão 2. Por fim, a primeira transação também deseja atualizar o dado e, como anteriormente com a segunda transação, é realizada uma comparação entre as versões do dado na transação e no banco de dados. Porém, neste momento a versão na transação é 1 e no banco é 2, ou seja, não correspondem.

Sendo assim, um erro é disparado e a ação desejada pela primeira transação não é concretizada, evitando que a atualização realizada pela segunda transação seja desfeita/sobrescrita.

Ilustração 23 - Locking Otimista: Abordagem com Version Number

Com o Hibernate, uma forma de utilizar o versionamento dos dados é com a anotação @Version no mapeamento das tabelas. Dessa forma, entre os diversos atributos de uma classe um deles deve ser o que representará a versão atual das linhas da tabela, será do tipo inteiro e terá a anotação citada. A ilustração seguinte exemplifica esta forma de versionamento.

Ilustração 24 - Primeira estratégia de lock otimista

Outra forma de implementar o lock otimista é utilizando o atributo que representa a versão do dado como sendo do tipo timestamp. Neste caso, a classe Aluno ao invés de ter um atributo inteiro para guardar a versão do dado, teria uma atributo do tipo Java.util.Date para guardar o instante no tempo da última atualização. Assim, a classe de domínio com o mapeamento seria equivalente à mostrada na ilustração 25.

Ilustração 25 - Segunda estratégia de lock otimista

No exemplo acima, além da anotação @Version a classe Aluno também deve incluir @Temporal para anotar o atributo que será responsável pelo versionamento.

Se a tabela não possuir uma coluna para armazenar a versão do dado ou a data da última atualização, utilizando o Hibernate, há outra forma de implementar o lock otimista, mas essa abordagem só deve ser utilizada para objetos que são modificados e atualizados em uma mesma sessão (Session). Caso contrário, uma das duas abordagens citadas anteriormente deve ser usada.

Com essa última abordagem, quando uma determinada linha vai ser atualizada, o Hibernate verifica se os dados dessa linha correspondem ao mesmo dado que foi recuperado. Se afirmativo, a ação é realizada. Para isso, no mapeamento é necessário utilizar o atributo optimisticLock = OptimisticLockType.ALL da anotação @org.hibernate.annotations.Entity.

Ilustração 26 – Terceira estratégia de lock otimista

2.5.2 Lock pessimista

A estratégia de lock pessimista que proíbe o acesso concorrente a um mesmo registro do banco de dados é efetuada bloqueando ele até que a transação seja finalizada.

O Hibernate fornece um conjunto de modos de lock (constantes disponíveis na classe LockMode) que podem ser utilizados para implementar o lock pessimista.

Por exemplo, o usuário do sistema de uma determinada escola realiza uma consulta na base de dados e altera o nome no registro do aluno consultado. Se não há um bloqueio ao dado, qualquer outra transação pode ter acesso a este mesmo registro concorrentemente e modificá-lo, podendo ocorrer uma inconsistência dos dados. A ilustração a seguir ilustra este exemplo.

Ilustração 27 - Transação sem lock

A próxima ilustração é um exemplo de uso do lock pessimista com Hibernate para resolver o problema mencionado no exemplo anterior.

Ilustração 28 - Transação com lock

O método get do objeto Session pode receber como terceiro argumento para implementar o lock pessimista as constantes listadas abaixo:

  • Lock.NONE: só realiza a consulta ao banco de dados se o objeto estiver no cache.

  • Lock.READ: ignora os dados no cache e faz verificação de versão para assegurar-se de que o objeto em memória é o mesmo que está no banco.

  • Lock.UPGRADE: ignora os dados no cache, faz verificação de versão e obtém lock pessimista do banco (se suportado).

  • Lock.UPGARDE_NOWAIT: mesmo que UPGRADE, mas desabilita a espera por liberação de locks e dispara uma exceção se o lock não puder ser obtido.

2.6 Maior desempenho em aplicações com Hibernate

Normalmente, quando um projeto tem início e novas tecnologias são incorporadas, elas são as primeiras a serem “apedrejadas” caso a performance da aplicação diminua comparada a de outro projeto. Isso pode ocorrer caso o desenvolvedor não conheça bem a ferramenta que está trabalhando. Portanto, é importantíssimo conhecer a ferramenta de trabalho, neste caso o Hibernate, e saber tirar o maior proveito de suas funcionalidades.

Entre os meios que o Hibernate utiliza para aumentar a performance das aplicações que implementam o MOR, estão alternativas como: estratégia de fetching e estratégia de cache.

Uma estratégia de fetching é a estratégia que o Hibernate irá usar para buscar objetos associados se a aplicação precisar navegar pela associação. Estratégias de Fetch podem ser declaradas nos metadados de mapeamento objeto-relacional, ou sobrescritos por uma query HQL ou query com Criteria.

A estratégia de cache será melhor detalhada no próximo item deste trabalho.

2.6.1 Caching

O cache é uma técnica comumente utilizada para aprimorar o desempenho da aplicação no que diz respeito ao acesso ao banco de dados. Com o cache é possível fazer uma cópia local dos dados, evitando acesso ao banco sempre que a aplicação necessitar, por exemplo, recuperar dados que nunca ou raramente são alterados. O uso desta técnica não é indicado para manipulação de dados que mudam frequentemente ou de dados que são compartilhados com outras aplicações. A má escolha das classes persistentes que terão objetos em cache pode resultar inconsistências na base de dados. Existem três tipos principais de cache:

  • escopo de transação: utilizado no escopo da transação, ou seja, cada transação possui seu próprio cache. Duas transações diferentes não compartilham o mesmo cache.

  • escopo de processo: há compartilhamento de cache entre uma ou mais transações. Os dados no escopo do cache de uma transação podem ser acessados por uma outra transação que executa concorrentemente, podendo provocar implicações relacionadas ao nível de isolamento.

  • escopo de cluster: cache compartilhado por vários processos pertencentes a máquinas virtuais distintas e deve ser replicado por todos os nós do cluster.

Considerando o cache no escopo da transação, se na transação houver mais de uma consulta a dados com mesmas identidades de banco de dados, a mesma instância do objeto Java será retornada.

Pode ser também que o mecanismo de persistência opte por implementar identidade no escopo do processo, de forma que a identidade do objeto seja equivalente à identidade do banco de dados. Assim, se a consulta a dados em transações que executam concorrentemente for feita a partir de identificadores de banco de dados iguais, o resultado também será o mesmo objeto Java.

Outra forma de se proceder é retornar os dados em forma de novos objetos. Assim, cada transação teria seu próprio objeto Java representando o mesmo dado no banco.

No escopo de cluster, é necessário haver comunicação remota, em que os dados são sempre manipulados por cópias. Em geral, utiliza-se o JavaGroups, que consiste em uma plataforma utilizada como infraestrutura para a sincronização do cache no cluster.

Nas situações em que estratégias de MOR permitem que várias transações manipulem uma mesma instância de objeto persistente, é importante ter um controle de concorrência eficiente, por exemplo, bloqueando um dado enquanto ele não é atualizado. Utilizando o Hibernate, é possível ter um conjunto diferente de instâncias para cada transação, ou seja, tem-se identidade no escopo da transação.

2.6.1.1 Arquitetura de cache com Hibernate

A ilustração 29 representa a arquitetura de cache com Hibernate. Existem dois níveis de cache. O primeiro nível encontra-se em nível de uma sessão (Session), ou em nível de transação de banco de dados ou até mesmo de uma transação de aplicação. Este nível não pode ser desabilitado e garante a identidade do objeto dentro da sessão/transação. Dessa forma, dentro de uma mesma sessão, é garantida que se a aplicação requisitar o mesmo objeto persistente duas vezes, ela receberá de volta à mesma instância, evitando um tráfego maior na base de dados.

Com o uso do cache em nível de transação, nunca poderão existir diferentes representações do mesmo registro no banco de dados ao término de uma transação, tendo apenas um objeto representando qualquer registro. Alterações realizadas em um objeto, dentro de uma transação, são sempre visíveis para qualquer outro código executado dentro da mesma transação.

Quando são feitas consultas através dos métodos load( ), fiind( ), list( ), iterate( ) ou filter( ) o conteúdo do objeto é, antes de mais nada, procurado no nível de cache.

O segundo nível de cache é opcional e pode abranger o nível do escopo do processo ou do cluster. É um cache de estado e não de instâncias persistentes. Por ser opcional, é preciso configurar este nível de cache para ser usado na aplicação. Cada classe que precisar ter objetos em cache possui uma configuração individual, pois cada uma terá particularidade de mapeamento.

Ilustração 29 - Arquitetura de cache

O Cache Provider é utilizado para informar qual política de cache de segundo nível será utilizada na aplicação. Essa política é definida no arquivo hibernate.cfg.xml a partir da propriedade hibernate.cache.provider_classe e pode assumir algum dos valores abaixo:

  • EHCache (org.hibernate.cache.EhCacheProvider): usado para o gerenciamento de caches no escopo do processo. Possui uma boa documentação e é fácil de configurar. Além disso, suporta cache de consultas;

  • OSCache (org.hibernate.cache.OSCacheProvider): usado para o gerenciamento de caches no escopo do processo ou do cluster;

  • SwarmCache (org.hibernate.cache.SwarmCacheProvider): usado para o gerenciamento de caches no escopo do cluster. Não suporta cache de consultas.

  • JBossCache (org.hibernate.cache.TreeCacheProvider): usado para o gerenciamento de caches no escopo do cluster. É um serviço de replicação por cluster transacional. Suporta cache de consultas.

CONCLUSÃO

Com base nos conceitos apresentados ao longo deste trabalho, é possível entender a dificuldade que um desenvolvedor encontra quando um projeto possui uma camada de negócios implementada com uma linguagem orientada a objetos e um banco de dados relacional. Mas a solução para resolver a impedância entre o modelo de objetos e o modelo de dados relacional também é apresentada, ou seja, o conceito de mapeamento objeto-relacional foi esclarecido, bem como a otimização do mapeamento por meio de um framework, neste caso, o Hibernate.

Portanto, conhecer estes conceitos é essencial para definir a estrutura de um projeto de software. Inegavelmente, integrar o modelo de objetos com o modelo de dados relacional é a melhor e mais utilizada opção para o desenvolvimento de aplicações comerciais. O Hibernate é uma ferramenta muito utilizada para otimizar o mapeamento objeto-relacional e, oferece uma série de recursos para suportar o mapeamento de objetos, relacionamentos, herança etc. Além do mais, possibilita otimizar a concorrência entre transações e dispõe de recursos que aumentam o desempenho na manipulação de dados. Dessa forma, estudar e utilizar esta ferramenta, sem dúvidas, enriquecerá o projeto de software e trará maiores resultados à aplicação final.

REFERÊNCIAS

AMBLER, Scott W. Agile Database Techniques. 1.ed. Nova Yorque: Wiley & Sons, 2003.

BAUER, Christian; KING, Gavin. Hibernate in Action. 1. ed.Greenwich: Manning, 2004.

COELHO, Camila Arnellas; SARTORELLI, Reinaldo Coelho. Persistência de Objetos Via Mapeamento Objeto-Relacional. 2004. 86p. Trabalho para Conclusão de Curso (Bacharelado) – Universidade Presbiteriana Mackenzie, Faculdade de Computação e Informática, São Paulo, 2004. Disponível em: <http://www.livrosgratis.com.br/arquivos_livros/ea000253.pdf> Acesso em: 03 out 2009.

FERNANDES, Raphaela Galhardo; LIMA, Gleydson de A. Ferreira. Anotações com Hibernate. 2007. 99p. Natal, 2007. Disponível em: <ftp://users.dca.ufrn.br/UnP2007/Hibernate_Anotacoes.pdf > Acesso em: 10 out 2009.

Hibernate Reference Documentation. Disponível em: <https://www.hibernate.org/5.html> Acesso em: 11 ago 2009.

MALDONADO, J. C. et al. Padrões e Frameworks de Software. 32p. Universidade de São Paulo, Instituto de Ciências Matemáticas e de Computação. Disponível em: <http://www.icmc.sc.usp.br/~rtvb/apostila.pdf> Acesso em: 10 out 2009.

1 Tokens são símbolos utilizados para distinguir elementos específicos de uma linguagem de programação.

2 Java Database Connectivity é um conjunto de classes e interfaces escritas em Java. Será explicado no decorrer deste trabalho.

3 Threadsafe: código ou programa que, se chamado por múltiplas linhas de execução (multithread), mantém seu estado válido.

4 O cache é uma técnica comumente utilizada para aprimorar o desempenho da aplicação no que diz respeito ao acesso ao banco de dados. Este conceito será apresentado no decorrer do trabalho.

5 Pool de conexões ou Connection pooling é uma técnica que proporciona a uma aplicação reutilizar conexões que existem em um pool ao invés de fechar e criar novas conexões repetidamente.

Comentários