Hibernate: Persistência de Objectos em Bases de Dados Relacionais

Table of Contents

1 Introdução

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

 

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.

2.4 Mapa de identidade

Um mapa de identidades guarda um registo de todos os objectos lidos da base de dados numa dada unidade de trabalho. Sempre que se pretende aceder a um objecto é feita uma pesquisa no mapa e o objecto devolvido. Caso o objecto ainda não tenha sido lido da base de dados, é-o no momento e o mapa é actualizado, sendo o objecto recém-criado o devolvido.

Este padrão é utilizado para garantir que não há múltiplos objectos em memória que representem a mesma informação na base de dados (para uma dada unidade de trabalho). Uma vantagem acrescida é a eliminação de acessos redundantes à base de dados, uma vez que, quando é necessário obter um objecto previamente lido, já não é necessário ler a informação novamente da base de dados.

2.5 Política de concorrência optimista

Uma consequência dos objectos não serem partilhados entre unidades de trabalho é a de que não há acesso concorrente aos objectos em memória. No entanto, ao nível da base de dados continuam a poder ocorrer acessos concorrentes a registos, bastando para tal que duas unidades de trabalho distintas estejam a manipular os mesmos objectos persistentes.

O Hibernate advoga uma política de concorrência optimista, em que se assume que a maioria das transacções de base de dados não entram em conflito com outras transacções, não sendo portanto necessário obter trincos na base de dados. A detecção de conflictos é feita através de um número de versão associado aos objectos persistentes (e gerido automaticamente pelo Hibernate). No commit da transacção é feita a validação do número de versão do objecto modificado contra o número de versão guardado no registo que lhe corresponde na base de dados.

Sem alterações adicionais aos objectos do modelo de domínio, o Hibernate não faz qualquer verificação de conflitos, pelo que, no caso de alteração concorrente de um objecto, será persistido o estado do objecto na última transacção a terminar.

Para que haja detecção automática de conflitos é necessário impor uma regra adicional sobre as classes que podem ser persistidas:  

2.6 Carregamento tardio de objectos

O carregamento tardio de objectos permite ler um objecto a partir do suporte persistente, mas não ler imediatamente os objectos que lhe estão associados. Os objectos associados só serão lidos se/quando acedidos pela primeira vez.

Para suportar o carregamento tardio, é usado o padrão Proxy2, em concreto a implementação já disponível na plataforma Java. Este padrão tem como requisito a classe que se quer carregar tardiamente poder ser substituida por um proxy cuja interface é a mesma da classe original, e cuja implementação se encarrega do carregamento tardio.

Este requisito impõe uma restrição adicional sobre os objectos que podem ser persistidos, caso se pretenda suportar o seu carregamento tardio:  

3 Mapeamento Objecto/Relacional

  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. Definir atributo de versão

    Um atributo que represente o número de versão do objecto deve ser anotado com @Version. De acordo com as regras previamente descritas, este atributo deve ser de um tipo numérico não primitivo (por exemplo, Long) e o seu nome deve seguir uma convenção (por exemplo, atributos de versão têm sempre o nome version):

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

  4. 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.1.1 Mapear herança

O Hibernate suporta três formas de mapear uma hierarquia de classes num modelo relacional:

  • Cada classe em sua tabela (TABLE_PER_CLASS);
  • Toda a hierarquia na mesma tabela (SINGLE_TABLE);
  • Cada subclasse em sua tabela (JOINED).

Para definir qual é o tipo de mapeamento escolhido, utiliza-se a anotação @Inheritance na classe de topo da hierarquia.

Nota: A anotação de interfaces não é suportada

Adicionalmente, o Hibernate permite definir a herança de atributos de uma super-classe quando esta não é persistente, através da anotação @MappedSuperclass. No exemplo que se segue, os valores dos atributos herdados name e weight serão guardados na mesma tabela que os atributos id e version (neste caso, a tabela Airplane descrita abaixo) quando uma instância de Airplane for persistida:

 @MappedSuperclass
 public class Aircraft {
     private String name;
     private long weight;

     ....
 }

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

     @Version
     private Long version;

     ...
 }

 CREATE TABLE Airplane (
     name       VARCHAR(255),
     weight     BIGINT  NOT NULL,
     id         BIGINT  NOT NULL  AUTO_INCREMENT  PRIMARY KEY,
     objVersion BIGINT  NOT NULL,
 );

3.1.1.1 Cada classe em sua tabela

Quando se usa uma estratégia uma tabela por classe, são definidas tantas tabelas quantas as classes da hierarquia. Em cada tabela são definidas colunas que permitem guardar todos os atributos persistentes da classe que lhe está associada (incluindo os herdados).

Esta estratégia impede o uso de alguns dos geradores automáticos de identificadores que o Hibernate disponibiliza (nomeadamente os AUTO e IDENTITY, que correspondem ao uso de uma coluna identificador gerida pelo sistema de gestão de base de dados3) dado que o identificador deve ser único entre as várias tabelas.

Adicionalmente, só são suportadas associações um-para-muitos se estas forem bidireccionais4. O exemplo que se segue define uma hierarquia com a classe Airplane e a sua subclasse Jet, que define um atributo adicional turbines que define o número de turbinas de um avião a jacto. A estratégia usando uma tabela por classe5 é apresentada de seguida, juntamente com as tabelas correspondentes:

 @Entity
 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
 public class Airplane {
     @Id
     private Long id;

     @Version
     private Long version;

     ...
 }

 @Entity
 public class Jet extends Airplane {
     private int turbines;

     ...
 }

 CREATE TABLE Airplane (
     id         BIGINT   NOT NULL  PRIMARY KEY,
     objVersion BIGINT   NOT NULL
 );

 CREATE TABLE Jet (
     id         BIGINT   NOT NULL  PRIMARY KEY,
     objVersion BIGINT   NOT NULL,
     turbines   INTEGER  NOT NULL
 );

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

3.1.1.2 Toda a hierarquia na mesma tabela

Utilizando esta estratégia, todos os atributos de todas as classes da hierarquia são mapeados na mesma tabela, sendo as instâncias distinguidas por uma coluna discriminante adicional.

Na classe topo da hierarquia é necessário, além de definir a estratégia de herança, identificar a coluna discriminante, através da anotação @DiscriminatorColumn (permite definir o nome, por omissão DTYPE, e o tipo da coluna, por omissão uma String). Cada classe deve ainda definir o valor da coluna discriminante, através da anotação @DiscriminatorValue (por omissão, o valor corresponde ao nome da classe persistente).

 @Entity
 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
 @DiscriminatorColumn(name="planeType", discriminatorType=DiscriminatorType.STRING)
 @DiscriminatorValue("Standard Airplane")
 public class Airplane {
     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Long id;

     @Version
     private Long version;

     ...
 }

 @DiscriminatorValue("Jet Aircraft")
 @Entity
 public class Jet extends Airplane {
     private int turbines;

     ...
 }

 CREATE TABLE Airplane (
     id         BIGINT   NOT NULL  AUTO_INCREMENT  PRIMARY KEY,
     planeType  VARCHAR(255) DEFAULT NULL,
     objVersion BIGINT   NOT NULL,
     turbines   INTEGER  NOT NULL
 );

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

3.1.1.3 Cada subclasse em sua tabela (joined subclass).

Nesta estratégia é definida uma tabela para cada classe da hierarquia, que contém apenas os atributos definidos nessa classe. Para se obterem as instâncias de uma dada classe é necessário fazer um join da tabela que representa a classe e de todas as tabelas que representam as classes acima na hierarquia.

As anotações @PrimaryKeyJoinColumn e @PrimaryKeyJoinColumns são usadas para definir, nas subclasses, a(s) chave(s) primária(s) a utilizar para efectuar o join. Por omissão, são utilizados os mesmos nomes de chave primária.

No exemplo que se segue todas as classes da hierarquia usam a estratégia uma tabela por subclasse. Entre as classes Airplane e Jet é feito um join usando os mesmos nomes para as chaves primárias. Entre as classes Airplane e RocketAircraft é feito um join usando a condição Airplane.id = RocketAircraft.planeId.

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

     @Version
     private Long version;

     ...
 }

 @Entity
 public class Jet extends Airplane {
     private int turbines;

     ...
 }

 @Entity
 @PrimaryKeyJoinColumn(name="planeId")
 public class RocketAircraft extends Airplane {
     private String rocketType;

     ...
 }

 CREATE TABLE Airplane (
     id         BIGINT   NOT NULL  AUTO_INCREMENT  PRIMARY KEY,
     objVersion BIGINT   NOT NULL
 );

 CREATE TABLE Jet (
     id         BIGINT   NOT NULL  AUTO_INCREMENT  PRIMARY KEY,
     turbines   INTEGER  NOT NULL
 );

 CREATE TABLE RocketAircraft (
     planeId    BIGINT   NOT NULL  AUTO_INCREMENT PRIMARY KEY,
     rocketType VARCHAR(255)
 );

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

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 Muitos-para-Um

  Uma associação muitos-para-um define-se através da anotação @ManyToOne no atributo que guarda a referência.

 @Entity
 public class Airline {
     @Id
     private Long id;
     ...
 }

 @Entity
 public class Airplane {
     ...

     @ManyToOne
     private Airline airline;

     ...
 }

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

 CREATE TABLE Airplane (
     ...
     airline_id BIGINT  REFERENCES Airline (id),
     ...
 );

Nota: Na ausência da anotação @JoinColumn, a coluna que guarda a chave estrangeira tem o valor de omissão: airline_id.

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

3.3.3 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.4 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.4.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.

Numa associação um-para-muitos bidireccional, a classe elemento é normalmente a responsável pela associação, pelo que corresponde a introduzir bidireccionalidade numa associação muitos-para-um (utilizando a anotação @OneToMany com o parâmetro mappedBy).

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

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

     @OneToMany(mappedBy="airline")
     private Set<Airplane> airplanes;
     ...
 }

 @Entity
 public class Airplane {
     ...

     @ManyToOne
     private Airline airline;

     ...
 }

Nota: A bidireccionalidade de uma associação muitos-para-um não trás implicação alguma no modelo relacional correspondente.

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

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

3.3.4.2 Muitos-para-Muitos

Uma associação muitos-para-muitos corresponde sempre, no modelo relacional, a definir uma tabela intermédia para estabelecer a relação entre as tabelas que representam os dois lados da associação.

Para identificar uma associação deste tipo utiliza-se a anotação @ManyToMany, podendo ser definida explicitamente a tabela intermédia, como já descrito nas relações um-para-muitos unidireccionais.

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

     @ManyToMany
     private Set<Passenger> passengers;

     ...
 }

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

     ...
 }

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

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

 CREATE TABLE Airplane_Passenger (
     Airplane_id  BIGINT  NOT NULL  REFERENCES Airplane (id),
     passengers_id BIGINT  NOT NULL  REFERENCES Passenger (id)
     PRIMARY KEY (Airplane_id, passengers_id),
 );

De forma semelhante ao que se faz nas restantes associações, introduzir bidireccionalidade corresponde a anotar, na classe que não é responsável pela relação, o atributo que representa a associação com @ManyToMany parametrizado com mappedBy:

 @Entity
 public class Airplane {
     ...

     @ManyToMany
     private Set<Passenger> passengers;

     ...
 }

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

     @ManyToMany(mappedBy="passengers")
     private Set<Airplane> airplanes;

     ...
 }

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

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

Exemplo: associação muitos-para-muitos entre Airplane e Passenger.

 

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:

As secções seguintes detalham cada um destes casos.

Nota: Num programa orientado a objectos as associações entre objectos são o mecanismo natural para obter referências para outros objectos. Embora se possam invocar aspectos de eficiência de execução para justificar o carregamento de objectos através do atributo identificador ou de pesquisa na base de dados, os custos a nível de manutenção do código aumentam substancialmente visto haver uma maior dependência do mecanismo de persistência/acesso. Por esta razão, o uso dos mecanismos de acesso a objectos fornecidos pelo Hibernate deve ser bem ponderado (idealmente apenas os objectos raiz do grafo de objectos precisam de ser carregados directamente).

4.2.1 Conhecendo o seu identificador

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.2.2 Por pesquisa

Quando se pretendem obter vários objectos que partilham uma dada característica (ou o identificador do objecto que pretendemos não é conhecido a priori), o Hibernate disponibiliza duas formas de pesquisa: uma linguagem de pesquisa (HQL) e uma interface programática (Query By Criteria & Example).

Para fazer uma pesquisa utilizando a interface programática basta criar uma nova instância de Criteria (através de uma instância de Session), parametrizada para a classe a pesquisar, e invocar um dos métodos list ou uniqueResult para obter os resultados da pesquisa.

Adicionar uma restrição à pesquisa faz-se através do método add, que recebe como argumento a restrição (as restrições possíveis são tipicamente obtidas através da classe Restrictions). Por fim é possível ordenar os resultados através do método addOrder que recebe como argumento o tipo de ordenação desejado.

 List<Airplane> planesForMaintenance = session.createCriteria(Airplane.class)
                                            .add( Restrictions.eq("state", "Maintenance due") )
                                            .addOrder( Order.asc("lastMaintenance") )
                                            .addOrder( Order.asc("name") )
                                            .list();

Neste exemplo, pretende-se a lista de todos os aviões que necessitam de manutenção, ordenados por data da última manutenção e nome do avião.

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

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ção6.

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

     // obtain the only airline available
     Airline airline = (Airline) session.createCriteria(Airline.class).uniqueResult();

     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 as classes persistentes. Para o fazer é necessário redefinir, no build.xml, o alvo -replace-hibernate-custom-tokens(dir) 7.

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

Complete o mapeamento objecto/relacional e garanta que as operações fornecidas pelas aplicações 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 "Design Patterns: Elements of Reusable Object-Oriented Software", E. Gamma, R. Helm, R. Johnson e J. Vlissides, Addison-Wesley Professional, 2004.

3 Por exemplo, chaves primárias com a propriedade AUTO_INCREMENT (no MySQL) ou IDENTITY (em SQL Server).

4 Porque não é necessária uma tabela intermédia para estabelecer a relação, em que uma das colunas precisaria de ser chave estrangeira para várias tabelas diferentes.

5 No exemplo, não é definido nenhum gerador, pelo que terá de ser a própria aplicação a gerir os identificadores, garantindo a sua unicidade.

6 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.

7 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:36 AM