Hibernate: (Introdução à) Persistência de Objectos em Bases de Dados Relacionais
1.2 Características do Hibernate
2.1 Unidade de trabalho
2.2 Modelo de domínio
2.3 Mapeamento Objecto/Relacional
3 Mapeamento Objecto/Relacional
3.1 Declarar uma classe persistente
3.2 Mapear atributos simples
3.3 Mapear associações entre classes persistentes
4 Manipulação de objectos persistentes
4.3 Modificaçã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.
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.
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.
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:
gerar as query SQL que obterão da base de dados o registo que contém os dados do objecto;
preencher os atributos do objecto com base no registo lido da base de dados.
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:
Identificar classe como persistente
Para identificar uma classe como persistente basta anotá-la com @Entity, como se mostra no exemplo:
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:
@Table(name=AIRPLANE_DATA)
Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#entity-mapping-entity
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):
Documentação: http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#entity-mapping-identifier
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.
<mapping class="examples.hibernate.domain.Airplane" />
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.
private static int airplaneCount;        // transient property
private transient double weightInPounds; // transient property
private String currentPilot;             // transient property
private long wingSpan;                   // persistent property
private long length;                     // persistent property
private AirplaneState state;             // enum persisted as String in database
private Date lastMaintenance;            // persistent property
private String specs;                    // persistent property persisted as a CLOB
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):
@Column(name="WING_SPAN")
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.
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.
@JoinColumn(name="engineNumber")
engineNumber BIGINT  UNIQUE  REFERENCES Engine (id),
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:
@OneToOne(mappedBy="engine")
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.
name = "Fleet",
joinColumns = @JoinColumn(name = "airlineCode"),
inverseJoinColumns = @JoinColumn(name = "planeNumber")
private Set<Airplane> airplanes;
id          BIGINT  NOT NULL  PRIMARY KEY,
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
tx = session.beginTransaction();            // start transaction
...                                         // do some work
tx.commit();                                // commit transaction & end unit of work
if (tx != null) tx.rollback();              // abort transaction
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:
carregamento directo, através do identificador do objecto que se pretende ler;
pesquisa, usando restrições sobre os valores das propriedades dos objectos que se pretendem3.
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:
removê-lo dos aviões da companhia aérea;
tornar o objecto transiente, para que deixe de ter uma representação persistente.
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º Exercício (uma única classe)
2º Exercício (duas classes numa associação um-para-um)
Bibliotecas necessárias (descomprimir para o directório do exercício)
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.