Hibernate: (Introdução à) Persistência de Objectos em Bases de Dados Relacionais

Table of Contents

1 Introdução

Este documento pretende ser uma primeira aproximação ao tema da persistência de objectos, para uma versão mais detalhada (e complexa) deve consultar-se este documento.

1.1 Objectivo

Manter, numa base de dados relacional, o estado de uma aplicação orientada a objectos feita em Java, com um mínimo de alterações ao modelo de programação.

1.2 Características do Hibernate

O Hibernate é uma framework de open source para persistência de objectos em bases de dados relacionais. Na génese deste tipo de ferramentas está a diferença entre os paradigmas relacional (usado pela maioria dos Sistemas de Gestão de Bases de Dados) e orientado a objectos (usado pelas aplicações Java), e a necessidade de estabelecer uma correspondência entre os dois modelos de dados (mapeamento objecto/relacional). Este mapeamento é posteriormente utilizado pelo Hibernate para suportar a leitura e escrita de objectos em suporte persistente no contexto de uma unidade de trabalho.1

2 Conceitos

 

Nesta secção apresentam-se os conceitos essenciais à compreensão do Hibernate. Para uma versão mais detalhada deve consultar-se este documento.

2.1 Unidade de trabalho

  Uma unidade de trabalho regista todas as operações de uma transacção de negócio que podem afectar a base de dados. Quando uma unidade de trabalho termina com sucesso, as operações registadas são propagadas para a base de dados. Se a unidade de trabalho abortou, todas as operações registadas são descartadas, como se a unidade de trabalho nunca tivesse existido.

Nota: Apenas as operações realizadas numa unidade de trabalho activa são executadas no contexto de uma transacção de base de dados.

2.2 Modelo de domínio

O modelo de domínio de uma aplicação é o conjunto de classes (dados e comportamento), e suas relações, que representa os conceitos manipulados pela aplicação. É este conjunto de classes que necessita de ser mantido de forma persistente através de uma framework como o Hibernate.

2.2.1 Plain Old Java Objects (POJO)

Qualquer objecto Java com dados e funcionalidade associada, pode ser tornado persistente, não sendo necessário fazer alterações de fundo ao modelo de domínio para suportar a sua persistência. Para tal basta:  

  • Definir um construtor sem argumentos (com, pelo menos, visibilidade package);
  • Definir um atributo identificador que guardará a sua identidade persistente (chave primária numa base de dados relacional). Este atributo deve:
    • ser de um tipo não primitivo (nullable);
    • seguir uma política de nomes consistente.

É ainda política recomendada a definição de propriedades JavaBeans com a convenção para os nomes dos métodos acessores (getXXXX, e isXXXX para predicados), modificadores (setXXXX) associados aos atributos persistentes dos objectos.

2.2.2 Estados possíveis de instâncias

Um objecto no estado transiente (transient) é um objecto instanciado pela aplicação, que não está, nem nunca esteve, associado a uma unidade de trabalho. Os seus dados não estão armazenados de forma persistente nem tem uma identidade persistente associada.

Um objecto está no estado persistente (persistent) quando tem uma identidade persistente associada e os seus dados estão (ou serão) armazenados de forma persistente. Um objecto persistente está associado a uma única unidade de trabalho, garantindo-se que, para essa unidade de trabalho, a identidade persistente é equivalente à identidade Java (referência para o objecto em memória).

Nota: Podem existir múltiplas instâncias do mesmo objecto persistente, desde que em unidades de trabalho diferentes.

Um objecto destacado (detached) é um objecto que já esteve associado a uma unidade de trabalho, mas a associação já não existe (a unidade de trabalho terminou ou o objecto foi explicitamente destacado). Tem uma identidade persistente, mas não há garantia alguma quanto à relação entre a identidade persistente e a identidade Java. Podem existir múltiplos objectos destacados com a mesma identidade persistente.

2.3 Mapeamento Objecto/Relacional

Esta correspondência é feita através de meta-informação associada às classes do modelo de domínio, através de anotações e/ou de ficheiros de configuração em XML.

Sempre que um objecto persistente é acedido pela primeira vez, o Hibernate usa a meta-informação associada à sua classe para:

Quando uma unidade de trabalho termina, a meta-informação sobre a correspondência entre o modelo de domínio e a sua representação relacional é utilizada pelo Hibernate para fazer as validações necessárias e gerar as queries SQL que efectuarão as alterações necessárias aos registos da base de dados.

3 Mapeamento Objecto/Relacional

 

Nesta secção apresentam-se as aspectos básicos essenciais ao mapeamento objecto/relacional. Para uma versão mais detalhada deve consultar-se este documento.

O mapeamento Objecto/Relacional faz-se com recurso a anotações do package javax.persistence, pelo que se aconselha a inclusão da linha seguinte na zona de imports de cada classe anotada:

 import javax.persistence.*;

3.1 Declarar uma classe persistente

 

  1. Identificar classe como persistente

    Para identificar uma classe como persistente basta anotá-la com @Entity, como se mostra no exemplo:

     @Entity
     public class Airplane {
         ...
     }
    

    Por omissão, o Hibernate associa a classe persistente a uma tabela com o mesmo nome. Caso este comportamento não seja desejável, é possível definir a tabela onde serão guardados os dados dos objectos usando a anotação @Table:

     @Entity
     @Table(name=AIRPLANE_DATA)
     public class Airplane {
         ...
     }
    

    Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#entity-mapping-entity

  2. Definir atributo identificador da classe.

    Para definir um atributo como identificador da classe é necessário anotá-lo com @Id e, caso se queiram identificadores gerados automaticamente pelo Hibernate (fortemente recomendado), @GeneratedValue. Seguindo as regras previamente descritas, o atributo não deve ser de um tipo primitivo (por exemplo, Long) e o seu nome deve seguir uma convenção (por exemplo, atributos identificadores têm sempre o nome id):

     @Entity
     public class Airplane {
         @Id @GeneratedValue(strategy=GenerationType.AUTO)
         private Long id;
    
         ...
     }
    

    Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#entity-mapping-identifier

  3. Actualizar o ficheiro de configuração do Hibernate

    Para dar a conhecer ao Hibernate o novo tipo de objectos persistentes é necessário alterar o ficheiro hibernate.cfg.xml para que inclua a classe recém-anotada.

     ...
    
     <hibernate-configuration>
         <session-factory>
             ...
             <mapping class="examples.hibernate.domain.Airplane" />
             ...
         </session-factory>
     </hibernate-configuration>
    

    Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/ch01.html#setup-configuration

3.2 Mapear atributos simples

Todos os atributos não estáticos e não transientes de um objecto persistente são por omissão persistentes, excepto se anotados com @Transient. É possível identificar explicitamente um atributo como persistente utilizando a anotação @Basic (que permite, adicionalmente, definir a politica de carregamento de atributos).

O Hibernate suporta directamente o mapeamento de todos os tipos primitivos do Java, suas classes wrapper e qualquer classe serializável.

Valores Enum podem ser representados numa coluna quer como ordinais (é guardado o ordinal do tipo enumerado), quer como texto (é guardada a representação textual do tipo). Por omissão é utilizada a representação ordinal para enumerados, mas pode definir-se explicitamente o tipo de representação (ORDINAL ou STRING) através da anotação @Enumerated.

Em Java a precisão de um atributo temporal não está definida, pelo que a mesma classe (java.util.Date) pode representar uma data, um instante ao longo do dia ou ambos (um instante numa dada data). A semântica do atributo temporal é no entanto relevante para a representação em base de dados, pelo que se pode usar a anotação @Temporal para a definir (DATE para uma data, TIME para um instante ao longo do dia e TIMESTAMP para um instante numa dada data). Por omissão, o Hibernate assume a precisão máxima para atributos temporais.

Caso se pretenda guardar uma sequência de bytes ou caracteres de grande dimensão pode usar-se a anotação @Lob do Hibernate.

 @Entity
 public class Airplane {
     ...

     private static int airplaneCount;        // transient property

     private transient double weightInPounds; // transient property

     @Transient
     private String currentPilot;             // transient property

     private long wingSpan;                   // persistent property

     @Basic
     private long length;                     // persistent property

     @Enumerated(EnumType.STRING)
     private AirplaneState state;             // enum persisted as String in database

     @Temporal(TemporalType.DATE)
     private Date lastMaintenance;            // persistent property

     @Lob
     private String specs;                    // persistent property persisted as a CLOB

     ...
 }

 CREATE TABLE Airplane (
     ...
     wingSpan        BIGINT NOT NULL,
     length          BIGINT NOT NULL,
     state           VARCHAR(255),
     lastMaintenance DATE,
     specs           TEXT
     ...
 );

No exemplo anterior, os valores dos atributos airplaneCount (atributo de classe), weightInPounds (um atributo transiente) e currentPilot (anotado com @Transient) não serão guardados na base de dados. wingSpan e length são atributos persistentes que serão guardados na base de dados. O atributo maintenanceStatus é um enumerado, e será guardada na base de dados a sua representação textual. Finalmente o atributo lastMaintenance representa o dia da última manutenção, pelo que apenas será guardada na base de dados a informação relativa à data (não sendo relevante o instante em que ocorreu).

Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#d0e304

3.2.1 Atributos de Colunas da BD

Por omissão, os atributos persistentes são guardados em colunas com o nome do atributo. Caso tal não seja desejado, pode utilizar-se a anotação @Column para definir explicitamente o nome da coluna onde o valor do atributo será guardado (no exemplo, a coluna chamar-se-á WING_SPAN):

 @Entity
 public class Airplane {
     ...

     @Column(name="WING_SPAN")
     private long wingSpan;

     ...
 }

Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#entity-mapping-property-column

3.3 Mapear associações entre classes persistentes

  Quando existem associações entre um (ou mais) objecto(s) persistente(s) e um outro objecto persistente, um dos objectos (objecto origem) guarda uma referência para o outro (objecto destino). No modelo relacional isto equivale à tabela que corresponde ao objecto origem ter uma coluna com uma chave estrangeira para a tabela que corresponde ao objecto destino, cuja declaração é suportada pela anotação @JoinColumn.

Por omissão é gerada uma coluna cujo nome é composto pelo nome do atributo, no objecto origem, que mantém a referência para o objecto destino, seguido do carácter _ (underscore), seguido do nome do atributo identificador da classe destino.

3.3.1 Um-para-Um

  Para criar uma associação um-para-um entre dois objectos persistentes usa-se a anotação @OneToOne no atributo que guarda a referência.

 @Entity
 public class Airplane {
     ...

     @OneToOne
     @JoinColumn(name="engineNumber")
     private Engine engine;

     ...
 }

 @Entity
 public class Engine {
     @Id
     private Long id;

     ...
 }

 CREATE TABLE Airplane (
     ...
     engineNumber BIGINT  UNIQUE  REFERENCES Engine (id),
     ...
 );

 CREATE TABLE Engine (
     id           BIGINT  NOT NULL  PRIMARY KEY,
     ...
 );

Nota: No exemplo, o uso da anotação @JoinColumn define explicitamente o nome da coluna que guarda a chave de estrangeira como engineNumber.

Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#d0e998

3.3.2 Associações bidireccionais

Quando uma associação é navegável nos dois sentidos, ambos os lados da associação guardam uma referência para o outro lado da associação. Em Hibernate, todas as anotações que definem associações podem receber um parâmetro mappedBy, que define o nome do atributo, no objecto responsável pela associação, onde a associação está definida.

Mantendo o exemplo da associação um-para-um, introduzir bidireccionalidade corresponde a estabelecer a relação um-para-um no sentido inverso (de Engine para Airplane), indicando que Airplane é responsável pela actualização do estado da associação:

 @Entity
 public class Airplane {
     ...

     @OneToOne
     @JoinColumn(name="engineNumber")
     private Engine engine;

     ...
 }

 @Entity
 public class Engine {
     @Id
     private Long id;

     @OneToOne(mappedBy="engine")
     private Airplane airplane;
     ...
 }

Nota: A bidireccionalidade de uma associação não traz implicação alguma no modelo relacional correspondente.

Exemplo: associação um-para-um entre Airplane e Engine.

3.3.3 Colecções

Uma colecção corresponde a uma associação entre um (ou mais) objecto(s) e um conjunto arbitrário de objectos. O Hibernate permite mapear as várias semânticas disponibilizadas na plataforma Java.

Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#entity-mapping-association-collections

3.3.3.1 Um-para-Muitos

  Tendo uma associação um-para-muitos unidireccional, tipicamente faz sentido o elemento unitário ser o responsável pela relação, pelo que o modelo relacional necessita de uma tabela intermédia para representar a relação.

A tabela intermédia define-se através da anotação @JoinTable, que recebe como parâmetros o nome da tabela (name), e o nome das colunas que guardam chaves estrangeiras para cada uma das tabelas relacionadas:

  • joinColumns define a(s) coluna(s) que guarda(m) a chave estrangeira para a tabela que representa o objecto responsável;
  • inverseJoinColumns que define o nome da(s) coluna(s) que guarda(m) a chave estrangeira para a tabela que representa o objecto elemento da colecção.

     @Entity
     public class Airline {
         @Id
         private Long id;
    
         @OneToMany
         @JoinTable(
             name = "Fleet",
             joinColumns = @JoinColumn(name = "airlineCode"),
             inverseJoinColumns = @JoinColumn(name = "planeNumber")
         )
         private Set<Airplane> airplanes;
         ...
     }
    
     @Entity
     public class Airplane {
         @Id
         private Long id;
    
         ...
     }
    

     CREATE TABLE Airline (
         id          BIGINT  NOT NULL  PRIMARY KEY,
         ...
     );
    
     CREATE TABLE Airplane (
         id          BIGINT  NOT NULL  PRIMARY KEY,
         ...
     );
    
     CREATE TABLE Fleet (
         airlineCode BIGINT  NOT NULL  REFERENCES Airline (id),
         planeNumber BIGINT  NOT NULL  UNIQUE  REFERENCES Airplane (id)
         PRIMARY KEY (airlineCode, planeNumber),
     );
    

Caso se omita a anotação @JoinTable, ou algum dos seus parâmetros:

  • name é definido como o nome da classe responsável, seguido do carácter _ (underscore), seguido do nome da classe elemento da colecção;
  • joinColumns é definido como o nome da classe responsável, seguido do carácter _ (underscore), seguido do nome da coluna que guarda a sua chave primária;
  • inverseJoinColumns é definido como o nome do atributo que define a associação na classe responsável, seguido do carácter _ (underscore), seguido do nome da coluna que guarda a chave primária da classe elemento.

4 Manipulação de objectos persistentes

Uma vez definido o mapeamento do modelo de domínio no modelo relacional correspondente, é agora necessário descrever o ciclo de vida de um objecto persistente, desde a sua criação até à sua destruição. A documentação do Hibernate tem um resumo sumário de como criar (e ler) um objecto persistente.

Uma unidade de trabalho é representada por uma instância de Session. Para se obter uma instância de Session utiliza-se uma fábrica SessionFactory fornecida pelo Hibernate e previamente configurada. Esta configuração é tipicamente feita numa classe auxiliar, cujo código não repetimos aqui. O método getCurrentSession de uma SessionFactory devolve a unidade de trabalho corrente, isto é, a instância de Session associada ao fio de execução em que o método é invocado (e criando uma nova caso não haja nenhuma).

É através de uma Session que se pode iniciar uma transacção de base de dados. Quando a transacção termina (com ou sem sucesso) o Hibernate termina também a unidade de trabalho corrente. Quaisquer operações que envolvam comunicação com a base de dados (independentemente de se uma leitura ou escrita) têm de ser realizadas no contexto de uma transacção.

Assume-se que os restantes exemplos desta secção se executam no contexto de uma transacção obtida segundo o seguinte padrão:

 Session session = factory.getCurrentSession();  // obtain/start unit of work
 Transaction tx = null;
 try {
     tx = session.beginTransaction();            // start transaction

     ...                                         // do some work

     tx.commit();                                // commit transaction & end unit of work
 } catch (RuntimeException ex) {
     if (tx != null) tx.rollback();              // abort transaction
     throw ex;
 }

4.1 Criação

Um objecto recém-criado encontra-se no estado transiente, sendo necessário associá-lo a uma unidade de trabalho do Hibernate para o tornar persistente. Esta associação é feita através do método save de uma instância de Session:

     ...
     Airplane airplane = new Airplane("Air Force 1", ...);
     session.save(airplane);                    // make object persistent
     ...

Nota: Uma vez que foi definido um gerador automático de identificadores para Airplane, só após a invocação do método save será atribuido o identificador à instância do objecto.

Documentação: http://www.hibernate.org/hib_docs/v3/reference/en/html/objectstate.html#objectstate-makingpersistent

4.2 Leitura

  O Hibernate permite obter objectos persistentes através de dois mecanismos complementares:

Para obter um objecto persistente do qual sabemos previamente o identificador podemos usar o método load de uma instância de Session. O método load recebe como argumentos a classe a ler e o identificador a utilizar e devolve um novo objecto persistente dessa classe, com os dados obtidos da base de dados.

     ...
     Long id = new Long(554);                  // load does not accept primitive types
     Airplane plane = (Airplane) session.load(Airplane.class, id);
     ...

Nota: Se não existir um registo na tabela Airplane da base de dados com o identificador 554, o método load lança uma excepção.

Documentação: http://www.hibernate.org/hib_docs/v3/reference/en/html/objectstate.html#objectstate-loading

4.3 Modificação

Ao manipular um objecto persistente no contexto de uma unidade de trabalho, as alterações serão automaticamente detectadas e persistidas quando a unidade de trabalho terminar.

Documentação: http://www.hibernate.org/hib_docs/v3/reference/en/html/objectstate.html#objectstate-modifying

4.4 Eliminação

Um objecto persistente pode ser tornado transiente (eliminado da base de dados) através do método delete na classe Session. É no entanto importante referir que as referências em memória se mantêm, pelo que se mantém a necessidade de eliminar o objecto (agora transiente) do grafo de objectos da aplicação2.

     ...
     // obtain plane to remove
     Airplane plane = (Airplane) session.load(Airplane.class, new Long(554));

     // obtain airline
     Airline airline = (Airline) session.load(Airline.class, new Long(1));

     airline.removeAirplane(plane);             // remove all references to plane

     session.delete(plane);                     // make object transient
     ...

No exemplo, existe uma única companhia aérea (singleton), ao qual pertence o avião que pretendemos remover. Para eliminar o avião é necessário:

Documentação: http://www.hibernate.org/hib_docs/v3/reference/en/html/objectstate.html#objectstate-deleting

5 Exercício

Este exercício utiliza o ImportAnt, nomeadamente os fragmentos relativos a Hibernate e aplicações de linha de comandos. Para classes de domínio recém-anotadas, é necessária actualizar o ficheiro de configuração do Hibernate. O ImportAnt simplifica este processo, permitindo definir no build.xml, alvo -replace-hibernate-custom-tokens(dir) as classes persistentes4.

O alvo generate-db-schema, fornecido pelo fragmento hibernate.xml, deve ser utilizados para gerar um novo esquema de base de dados com base na informação das classes anotadas.

O alvo run, fornecido pelo fragmento console-app.xml é utilizado para executar uma aplicação de linha de comandos. É possível passar argumentos à aplicação através da propriedade run.args. Esta propriedade pode definir-se directamente na linha de comandos através da opção -D do Ant:

 $ ant -Drun.args=Create run

Faça o mapeamento objecto/relacional e garanta que a operações fornecidas pela aplicação utilizam adequadamente a base de dados.

6 Notas

1 O Hibernate pode ser utilizado quer quando já existe um modelo relacional com tabelas definidas (permitindo gerar classes a partir das tabelas); quer quando se parte do modelo de objectos (permitindo gerar as tabelas a partir das classes anotadas).

2 Eliminar um objecto corresponde a eliminar todas as referências para ele, de modo a que o Garbage Collector do Java possa libertar a memória que está associada ao objecto.

3 Para mais informação sobre esta possibilidade consultar-se este documento.

4 Durante o processo de build da aplicação esta informação será utilizada para actualizar o ficheiro hibernate.cfg.xml.

Date: 2008/03/05 10:34:19 AM