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.
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.
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
Nesta secção apresentam-se os conceitos essenciais à compreensão do Hibernate. Para uma versão mais detalhada deve consultar-se este documento.
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. |
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.
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:
É 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.
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.
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.
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.*;
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
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
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
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
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
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.
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
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
.
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
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.
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; }
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
O Hibernate permite obter objectos persistentes através de dois mecanismos complementares:
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). |
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
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
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
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.
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