Skip to content

2. Entidades JPA

2.1. Exemplo 1 - Representação de objeto de uma única tabela

2.1.1. A tabela [person]

Considere uma base de dados com uma única tabela [pessoa], cujo objetivo é armazenar algumas informações sobre indivíduos:

 
ID
chave primária da tabela
VERSÃO
versão da linha na tabela. Sempre que
a pessoa é modificada, o seu número de versão é incrementado.
NAME
apelido da pessoa
NOME
nome
DATA DE NASCIMENTO
a data de nascimento dela
MARIE
número inteiro 0 (solteira) ou 1 (casada)
NBENFANTS
número de filhos

2.1.2. A entidade [Pessoa]

Estamos no seguinte ambiente de execução:

A camada JPA [5] deve fazer a ponte entre o mundo relacional da base de dados [7] e o mundo dos objetos [4] manipulados pelos programas Java [3]. Esta ponte é estabelecida através da configuração, e há duas formas de o fazer:

  1. utilizando ficheiros XML. Esta era praticamente a única forma de o fazer até ao advento do JDK 1.5
  1. utilizando anotações Java desde o JDK 1.5

Neste documento, utilizaremos quase exclusivamente o segundo método.

O objeto [Person] que representa a tabela [person] apresentada anteriormente poderia ser o seguinte:


...
 
@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{
 
    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
 
    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;
 
    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;
 
    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(name = "MARIE", nullable = false)
    private boolean marie;
 
    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;
 
    // manufacturers
    public Personne() {
    }
 
    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
        setNom(nom);
        setPrenom(prenom);
        setDatenaissance(datenaissance);
        setMarie(marie);
        setNbenfants(nbenfants);
    }
 
    // toString
    public String toString() {
...
    }
 
    // getters and setters
...
}

A configuração é realizada utilizando anotações Java (@Annotation). As anotações Java são processadas pelo compilador ou por ferramentas especializadas em tempo de execução. Além da anotação na linha 3 destinada ao compilador, todas as anotações aqui se destinam à implementação JPA utilizada, seja Hibernate ou Toplink. Serão, portanto, processadas em tempo de execução. Na ausência de ferramentas capazes de as interpretar, estas anotações são ignoradas. Assim, a classe [Person] acima poderia ser utilizada num contexto não-JPA.

Existem dois casos distintos para a utilização de anotações JPA numa classe C associada a uma tabela T:

  1. a tabela T já existe: as anotações JPA devem então replicar a estrutura existente (nomes e definições de colunas, restrições de integridade, chaves estrangeiras, chaves primárias, etc.)
  2. A tabela T não existe e será criada com base nas anotações encontradas na classe C.

O caso 2 é o mais fácil de tratar. Utilizando anotações JPA, especificamos a estrutura da tabela T que pretendemos. O caso 1 é frequentemente mais complexo. A tabela T pode ter sido criada há muito tempo fora de qualquer contexto JPA. A sua estrutura pode, portanto, ser inadequada para a ponte relacional-objeto do JPA. Para simplificar as coisas, vamos concentrar-nos no caso 2, em que a tabela T associada à classe C será criada com base nas anotações JPA na classe C.

Vamos examinar as anotações JPA da classe [Person]:

  • linha 4: a anotação @Entity é a primeira anotação essencial. É colocada antes da linha que declara a classe e indica que a classe em questão deve ser gerida pela camada de persistência do JPA. Sem esta anotação, todas as outras anotações JPA seriam ignoradas.
  • linha 5: a anotação @Table designa a tabela da base de dados que a classe representa. O seu principal argumento é name, que especifica o nome da tabela. Sem este argumento, a tabela receberá o nome da classe, neste caso [Person]. No nosso exemplo, a anotação @Table é, portanto, desnecessária.
  • Linha 8: A anotação @Id é utilizada para designar o campo na classe que representa a chave primária da tabela. Esta anotação é obrigatória. Aqui, indica que o campo id na linha 11 representa a chave primária da tabela.
  • Linha 9: A anotação @Column é utilizada para associar um campo da classe à coluna da tabela que esse campo representa. O atributo name especifica o nome da coluna na tabela. Se este atributo for omitido, a coluna assume o mesmo nome do campo. No nosso exemplo, o argumento name não era, portanto, necessário. O argumento nullable=false indica que a coluna associada ao campo não pode ter o valor NULL e que o campo deve, portanto, ter um valor.
  • Linha 10: A anotação @GeneratedValue especifica como a chave primária é gerada quando é gerada automaticamente pelo SGBD. Este será o caso em todos os nossos exemplos. Não é obrigatória. Assim, a nossa Person poderia ter um ID de estudante que serve como chave primária e não é gerado pelo SGBD, mas definido pela aplicação. Neste caso, a anotação @GeneratedValue seria omitida. O argumento strategy especifica como a chave primária é gerada quando gerada pelo SGBD. Nem todos os SGBDs utilizam a mesma técnica para gerar valores de chave primária. Por exemplo:
Firebird
utiliza um gerador de valores chamado antes de cada inserção
SQL Server
o campo da chave primária é definido como tendo o tipo Identity. O resultado é semelhante ao gerador de valores do Firebird, exceto que o valor da chave só é conhecido após a inserção da linha.
Oracle
utiliza um objeto chamado SEQUENCE, que, mais uma vez, atua como um gerador de valores

A camada JPA deve gerar instruções SQL diferentes, dependendo do SGBD, para criar o gerador de valores. Especificamos o tipo de SGBD que ela precisa de tratar através da configuração. Como resultado, ela pode determinar a estratégia padrão para gerar valores de chave primária para esse SGBD. O argumento strategy = GenerationType.*****AUTO* indica à camada JPA para utilizar esta estratégia padrão. Esta técnica funcionou em todos os exemplos deste documento para os sete SGBDs utilizados.

  • Linha 14: A anotação @Version designa o campo utilizado para gerir o acesso simultâneo à mesma linha na tabela.

Para compreender esta questão do acesso simultâneo à mesma linha na tabela [person], vamos supor que uma aplicação web permite que as informações de uma pessoa sejam atualizadas e considerar o seguinte cenário:

No momento T1, o utilizador U1 começa a editar uma pessoa P. Neste momento, o número de filhos é 0. Ele altera este número para 1, mas antes de enviar as suas alterações, o utilizador U2 começa a editar a mesma pessoa P. Como U1 ainda não enviou as suas alterações, U2 vê o número de filhos como 0 no seu ecrã. U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 guardam as suas alterações nessa ordem. A alteração de U2 terá precedência: na base de dados, o nome estará em maiúsculas e o número de filhos permanecerá em zero, mesmo que U1 acredite que o tenha alterado para 1.

O conceito de versão de uma pessoa ajuda-nos a resolver este problema. Vamos revisitar o mesmo caso de uso:

No momento T1, um utilizador U1 começa a editar uma pessoa P. Nesta altura, o número de filhos é 0 e a versão é V1. Ele altera o número de filhos para 1, mas antes de confirmar a alteração, um utilizador U2 começa a editar a mesma pessoa P. Como U1 ainda não confirmou a alteração, U2 vê o número de filhos como 0 e a versão como V1. U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 confirmam as suas alterações nessa ordem. Antes de confirmar uma alteração, verificamos se o utilizador que está a modificar a pessoa P possui a mesma versão que a versão da pessoa P atualmente guardada no e . Este será o caso do utilizador U1. A sua alteração é, portanto, aceite e, em seguida, alteramos a versão da pessoa modificada de V1 para V2 para indicar que a pessoa sofreu uma alteração. Ao validar a modificação de U2, verificaremos que U2 tem a versão V1 da pessoa P, enquanto a versão atual é V2. Podemos então informar o utilizador U2 de que outra pessoa agiu antes dele e que deve começar com a nova versão da pessoa P. Ele fará isso, recuperará a versão V2 da pessoa P, que agora tem um filho, colocará o nome em maiúsculas e validará. A sua modificação será aceite se a pessoa P registada ainda estiver na versão V2. Em última análise, as modificações feitas por U1 e U2 serão tidas em conta, enquanto que no caso de utilização sem versões, uma das modificações teria sido perdida.

A camada [DAO] da aplicação cliente pode gerir a versão da própria classe [Person]. Sempre que um objeto P for modificado, a versão desse objeto será incrementada em 1 na tabela. A anotação @Version permite que esta gestão seja transferida para a camada JPA. O campo em questão não precisa de se chamar version, como no exemplo. Pode ter qualquer nome.

Os campos correspondentes às anotações @Id e @Version estão presentes para fins de persistência. Não seriam necessários se a classe [Person] não precisasse de ser persistida. Podemos ver, portanto, que um objeto é representado de forma diferente dependendo de precisar ou não de ser persistido.

  • Linha 17: Mais uma vez, a anotação @Column fornece informações sobre a coluna na tabela [person] associada ao campo name da classe Person. Aqui encontramos dois novos argumentos:
    • unique=true indica que o nome de uma pessoa deve ser único. Isto resultará na adição de uma restrição de unicidade na coluna NAME da tabela [person] na base de dados.
    • length=30 define o número de caracteres na coluna NAME para 30. Isto significa que o tipo desta coluna será VARCHAR(30).
  • Linha 24: A anotação @Temporal é utilizada para especificar o tipo SQL para uma coluna ou campo de data/hora. O tipo TemporalType.DATE denota uma data sem hora associada. Os outros tipos possíveis são TemporalType.TIME para codificar uma hora e TemporalType.TIMESTAMP para codificar uma data e hora.

Vamos agora comentar o resto do código na classe [Person]:

  • Linha 6: A classe implementa a interface Serializable. A serialização de um objeto consiste em convertê-lo numa sequência de bits. A deserialização é a operação inversa. A serialização/deserialização é particularmente utilizada em aplicações cliente/servidor, nas quais os objetos são trocados através da rede. As aplicações cliente ou servidor não têm conhecimento desta operação, que é realizada de forma transparente pelas JVMs. Para que isto seja possível, no entanto, as classes dos objetos trocados devem ser «marcadas» com a palavra-chave Serializable.
  • Linha 37: um construtor para a classe. Note-se que os campos id e version não estão incluídos entre os parâmetros. Isto deve-se ao facto de estes dois campos serem geridos pela camada JPA e não pela aplicação.
  • Linhas 51 e seguintes: os métodos get e set para cada um dos campos da classe. Note-se que as anotações JPA podem ser colocadas nos métodos get dos campos em vez de nos próprios campos. A localização das anotações indica o modo que o JPA deve utilizar para aceder aos campos:
    • se as anotações forem colocadas ao nível do campo, o JPA acederá diretamente aos campos para os ler ou gravar
    • se as anotações forem colocadas no nível get, o JPA acederá aos campos através dos métodos get/set para os ler ou gravar

A posição da anotação @Id determina a colocação das anotações JPA numa classe. Quando colocada ao nível do campo, indica acesso direto aos campos; quando colocada ao nível do get, indica acesso aos campos através dos métodos get e set. As outras anotações devem então ser colocadas da mesma forma que a anotação @Id.

2.1.3. O Projeto de Teste do Eclipse

Realizaremos as nossas primeiras experiências com a entidade [Person] anterior. Iremos realizá-las utilizando a seguinte arquitetura:

  • em [7]: a base de dados que será gerada com base nas anotações da entidade [Person], bem como em configurações adicionais especificadas num ficheiro denominado [persistence.xml]
  • em [5, 6]: uma camada JPA implementada pelo Hibernate
  • em [4]: a entidade [Person]
  • em [3]: um programa de teste baseado em consola

Iremos realizar várias experiências:

  • gerar o esquema da base de dados utilizando um script Ant e as Ferramentas Hibernate
  • gerar a base de dados e inicializá-la com alguns dados
  • interagir com a base de dados e realizar as quatro operações básicas na tabela [person] (inserir, atualizar, eliminar, consultar)

As ferramentas necessárias são as seguintes:

  • O Eclipse e os seus plugins descritos na Secção 5.2.
  • o projeto [hibernate-personnes-entites], que se encontra na pasta <examples>/hibernate/direct/personnes-entites
  • os vários SGBDs descritos nos apêndices (Secção 5 e seguintes).

O projeto Eclipse é o seguinte:

  • em [1]: a pasta do projeto Eclipse
  • em [2]: o projeto importado para o Eclipse (Arquivo / Importar)
  • em [3]: a entidade [Person] que está a ser testada
  • em [4]: os programas de teste
  • em [5]: [persistence.xml] é o ficheiro de configuração para a camada JPA
  • em [6]: as bibliotecas utilizadas. Estas foram descritas na secção 1.5.
  • em [8]: um script Ant que será utilizado para gerar a tabela associada à entidade [Person]
  • em [9]: os ficheiros [persistence.xml] para cada um dos SGBDs utilizados
  • em [10]: os esquemas da base de dados gerada para cada um dos SGBDs utilizados

Iremos descrever estes elementos um a um.

2.1.4. A entidade [Person] (2)

Estamos a fazer uma ligeira modificação à descrição anterior da entidade [Person], bem como a adicionar algumas informações adicionais:


package entites;
 
...
 
@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_personne")
public class Personne implements Serializable{
 
    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
 
    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;
 
    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;
 
    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(name = "MARIE", nullable = false)
    private boolean marie;
 
    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;
 
    // manufacturers
    public Personne() {
    }
 
    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
....
    }
 
    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }
 
    // getters and setters
...
}
  • linha 7: nomeamos a tabela associada à entidade [Person] como [jpa01_personne]. Neste documento, serão criadas várias tabelas num esquema sempre denominado jpa. No final deste tutorial, o esquema jpa conterá muitas tabelas. Para ajudar o leitor a acompanhar, as tabelas que estão relacionadas entre si terão o mesmo prefixo jpaxx_.
  • linha 45: um método [toString] para exibir um objeto [Person] na consola.

2.1.5. Configurar a camada de acesso aos dados

No projeto Eclipse acima, a camada JPA é configurada através do ficheiro [META-INF/persistence.xml]:

Em tempo de execução, o ficheiro [META-INF/persistence.xml] é procurado no classpath da aplicação. No nosso projeto Eclipse, todo o conteúdo da pasta [/src] [1] é copiado para uma pasta [/bin] [2]. Esta pasta faz parte do classpath do projeto. É por isso que o ficheiro [META-INF/persistence.xml] será encontrado quando a camada JPA se configurar.

Por predefinição, o Eclipse não coloca o código-fonte na pasta [/src] do projeto, mas diretamente na própria pasta do projeto. Todos os nossos projetos Eclipse serão configurados de forma a que os códigos-fonte fiquem em [/src] e as classes compiladas em [/bin], conforme mostrado na Secção 5.2.1.

Vamos examinar a configuração da camada JPA no ficheiro [persistence.xml] do nosso projeto:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Persistent classes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- logs SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  automatic schematic creation -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  properties DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Para compreender esta configuração, precisamos de rever a arquitetura de acesso aos dados da nossa aplicação:

  • o ficheiro [persistence.xml] configura as camadas [4, 5, 6]
  • [4]: Implementação do JPA pelo Hibernate
  • [5]: O Hibernate acede à base de dados através de um conjunto de ligações. Um conjunto de ligações é um conjunto de ligações abertas ao SGBD. Um SGBD é acedido por vários utilizadores, mas, por razões de desempenho, não pode exceder um limite N de ligações abertas simultaneamente. Um código bem escrito abre uma ligação ao SGBD pelo tempo mínimo necessário: executa comandos SQL e fecha a ligação. Faz isso repetidamente, sempre que precisa de trabalhar com a base de dados. O custo de abrir e fechar uma ligação não é insignificante, e é aqui que entra o conjunto de ligações. Quando a aplicação arranca, o conjunto de ligações abre N1 ligações ao SGBD. A aplicação solicita uma ligação aberta ao conjunto sempre que precisa de uma. A ligação é devolvida ao conjunto assim que a aplicação deixa de precisar dela, de preferência o mais rapidamente possível. A ligação não é encerrada e permanece disponível para o próximo utilizador. Um conjunto de ligações é, portanto, um sistema para partilhar ligações abertas.
  • [6]: o controlador JDBC para o SGBD que está a ser utilizado

Agora vamos ver como o ficheiro [persistence.xml] configura as camadas [4, 5, 6] acima:

  • linha 2: a tag raiz do ficheiro XML é <persistence>.
  • linha 3: <persistence-unit> é utilizado para definir uma unidade de persistência. Podem existir várias unidades de persistência. Cada uma tem um nome (atributo name) e um tipo de transação (atributo transaction-type). A aplicação acederá à unidade de persistência através do seu nome, neste caso jpa. O tipo de transação RESOURCE_LOCAL indica que a aplicação gere as transações com o próprio SGBD. Este será o caso aqui. Quando a aplicação é executada num contentor EJB3, pode utilizar o serviço de transações do contentor. Nesse caso, definiríamos transaction-type=JTA (Java Transaction API). JTA é o valor predefinido quando o atributo transaction-type é omitido.
  • Linha 5: A tag <provider> é utilizada para definir uma classe que implementa a interface [javax.persistence.spi.PersistenceProvider], o que permite à aplicação inicializar a camada de persistência . Como estamos a utilizar uma implementação JPA/Hibernate, a classe utilizada aqui é uma classe Hibernate.
  • Linha 6: A tag <properties> introduz propriedades específicas do fornecedor escolhido. Assim, dependendo de ter escolhido Hibernate, TopLink, Kodo, etc., terá propriedades diferentes. As seguintes são específicas do Hibernate.
  • Linha 8: Instrui o Hibernate a analisar o classpath do projeto para encontrar classes anotadas com @Entity, para que possa gerir essas classes. As classes @Entity também podem ser declaradas utilizando as tags <class>class_name</class>, diretamente sob a tag <persistence-unit>. É isso que faremos com o fornecedor JPA/Toplink.
  • As linhas 10–12, que aqui estão comentadas, configuram os registos da consola do Hibernate:
  • Linha 10: para ativar ou desativar a exibição das instruções SQL emitidas pelo Hibernate para o SGBD. Isto é muito útil durante a fase de aprendizagem. Devido à ponte relacional/objeto, a aplicação trabalha com objetos persistentes aos quais aplica operações como [persist, merge, remove]. É muito útil saber quais as instruções SQL que são efetivamente emitidas para estas operações. Ao estudá-las, aprende-se gradualmente a antecipar as instruções SQL que o Hibernate irá gerar ao realizar tais operações em objetos persistentes, e a ponte relacional/objeto começa a tomar forma na sua mente.
  • Linha 11: As instruções SQL exibidas na consola podem ser formatadas de forma organizada para facilitar a sua leitura
  • Linha 12: As instruções SQL exibidas também serão anotadas
  • As linhas 15–19 definem a camada JDBC (camada [6] na arquitetura):
  • linha 15: a classe do controlador JDBC para o SGBD, neste caso o MySQL5
  • linha 16: o URL da base de dados que está a ser utilizada
  • Linhas 17, 18: o nome de utilizador e a palavra-passe da ligação
  • Aqui utilizamos elementos explicados nos apêndices da secção 5.5. Recomenda-se ao leitor que leia esta secção sobre o MySQL 5.
  • linha 22: O Hibernate precisa de saber com que SGBD está a trabalhar. Isto porque todos os SGBDs têm extensões SQL proprietárias, tais como a sua própria forma de lidar com a geração automática de valores de chave primária, ... o que significa que o Hibernate precisa de saber com que SGBD está a trabalhar para lhe enviar comandos SQL que o SGBD compreenda. [MySQL5InnoDBDialect] refere-se ao SGBD MySQL5 com tabelas InnoDB que suportam transações.
  • As linhas 24–28 configuram o conjunto de ligações do c3p0 (camada [5] na arquitetura):
  • Linhas 24, 25: o número mínimo (padrão 3) e máximo de ligações (padrão 15) no conjunto. O número inicial padrão de ligações é 3.
  • Linha 26: tempo máximo de espera, em milissegundos, para um pedido de ligação do cliente. Após este tempo limite, o c3p0 lançará uma exceção.
  • Linha 27: para aceder à base de dados, o Hibernate utiliza instruções SQL preparadas (PreparedStatement) que o c3p0 pode armazenar em cache. Isto significa que, se a aplicação solicitar uma instrução SQL preparada que já se encontre no cache uma segunda vez, esta não precisará de ser preparada (a preparação de uma instrução SQL implica um custo) e será utilizada a que se encontra no cache. Aqui, especificamos o número máximo de instruções SQL preparadas que o cache pode conter, em todas as ligações (uma instrução SQL preparada pertence a uma única ligação).
  • Linha 28: Intervalo de verificação de validade da ligação em milissegundos. Uma ligação no conjunto pode tornar-se inválida por várias razões (o controlador JDBC invalida a ligação porque esta esteve inativa durante demasiado tempo, o controlador JDBC tem erros, etc.).
  • Linha 20: Aqui, especificamos que, quando a camada de persistência for inicializada, o esquema da base de dados para objetos @Entity deve ser gerado. O Hibernate dispõe agora de todas as ferramentas para gerar as instruções SQL para a criação das tabelas da base de dados:
  • a configuração dos objetos @Entity permite-lhe saber quais as tabelas a gerar
  • As linhas 15–18 e 24–28 permitem-lhe estabelecer uma ligação com o SGBD
  • a linha 22 indica-lhe qual o dialeto SQL a utilizar para gerar as tabelas

Assim, o ficheiro [persistence.xml] utilizado aqui recria uma nova base de dados a cada nova execução da aplicação. As tabelas são recriadas (create table) após serem eliminadas (drop table), caso existissem. Note-se que isto não é, obviamente, algo a fazer com uma base de dados de produção...

Os testes demonstraram que a fase de eliminação/criação das tabelas pode falhar. Este foi particularmente o caso quando, para o mesmo teste, mudámos de uma camada JPA/Hibernate para uma camada JPA/Toplink ou vice-versa. Partindo dos mesmos objetos @Entity, as duas implementações não geram exatamente as mesmas tabelas, geradores, sequências, etc., e por vezes aconteceu que a fase de eliminação/criação falhou, exigindo que as tabelas fossem eliminadas manualmente. A secção «Anexos», a partir do parágrafo 5, descreve as ferramentas disponíveis para realizar esta tarefa manualmente. Deve notar-se que a implementação JPA/Hibernate provou ser a mais eficiente durante esta fase inicial de criação do conteúdo da base de dados: as falhas eram raras.

As ferramentas utilizadas pela camada JPA/Hibernate encontram-se na biblioteca [jpa-hibernate], apresentada na secção 1.5, página 8. Os controladores JDBC necessários para aceder ao SGBD encontram-se na biblioteca [jpa-divers]. Estas duas bibliotecas foram adicionadas ao classpath do projeto aqui estudado. O seu conteúdo está resumido abaixo:

2.1.6. Gerar a base de dados com um script Ant

Como acabámos de ver, o Hibernate fornece ferramentas para gerar o esquema da base de dados para os objetos @Entity da aplicação. O Hibernate pode:

  • gerar o ficheiro de texto contendo as instruções SQL que criam a base de dados. Neste caso, apenas é utilizado o dialeto especificado em [persistence.xml].
  • criar as tabelas que representam os objetos @Entity na base de dados de destino definida em [persistence.xml]. Neste caso, é utilizado todo o ficheiro [persistence.xml].

Apresentaremos um script Ant capaz de gerar o esquema da base de dados para objetos @Entity. Este script não é da minha autoria: baseia-se num script semelhante de [ref1]. O Ant (Another Neat Tool) é uma ferramenta de tarefas em lote Java. Os scripts Ant não são fáceis de compreender para principiantes. Utilizaremos apenas um, aquele que estamos agora a comentar:

  • em [1]: a estrutura de diretórios dos exemplos deste tutorial.
  • em [2]: a pasta [people-entities] do projeto Eclipse atualmente em estudo
  • em [3]: a pasta <lib> que contém as cinco bibliotecas JAR definidas na secção 1.5.
  • em [4]: o arquivo [hibernate-tools.jar] necessário para uma das tarefas no script [ant-hibernate.xml] que iremos examinar.
  • em [5]: o projeto Eclipse e o script [ant-hibernate.xml]
  • em [6]: a pasta [src] do projeto

O script [ant-hibernate.xml] [5] utilizará os ficheiros JAR na pasta <lib> [3], especificamente o ficheiro [hibernate-tools.jar] [4] na pasta [lib/hibernate]. Reproduzimos a árvore de diretórios para que o leitor possa ver que, para encontrar a pasta [lib] a partir da pasta [people-entities] [2] no script [ant-hibernate.xml], é necessário seguir o caminho: ../../../lib.

Vamos examinar o script [ant-hibernate.xml]:


<project name="jpa-hibernate" default="compile" basedir=".">
 
    <!-- nom du projet et version -->
    <property name="proj.name" value="jpa-hibernate" />
    <property name="proj.shortname" value="jpa-hibernate" />
    <property name="version" value="1.0" />
 
    <!-- Propriété globales -->
    <property name="src.java.dir" value="src" />
    <property name="lib.dir" value="../../../lib" />
    <property name="build.dir" value="bin" />
 
    <!-- le Classpath du projet -->
    <path id="project.classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>
 
    <!-- les fichiers de configuration qui doivent être dans le classpath-->
    <patternset id="conf">
        <include name="**/*.xml" />
        <include name="**/*.properties" />
    </patternset>
 
    <!-- Nettoyage projet -->
    <target name="clean" description="Nettoyer le projet">
        <delete dir="${build.dir}" />
        <mkdir dir="${build.dir}" />
    </target>
 
    <!-- Compilation projet -->
<target name="compile" depends="clean">
        <javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
    </target>
 
    <!-- Copier les fichiers de configuration dans le classpath -->
    <target name="copyconf">
        <mkdir dir="${build.dir}" />
        <copy todir="${build.dir}">
            <fileset dir="${src.java.dir}">
                <patternset refid="conf" />
            </fileset>
        </copy>
    </target>
 
    <!-- Hibernate Tools -->
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />
 
    <!-- Générer la DDL de la base -->
    <target name="DDL" depends="compile, copyconf" description="Génération DDL base">
 
        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Utiliser META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- export -->
            <hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
 
    <!-- Générer la base -->
    <target name="BD" depends="compile, copyconf" description="Génération BD">
 
        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Utiliser META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- export -->
            <hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
</project>
  • Linha 1: O projeto [ant] chama-se «jpa-hibernate». É composto por um conjunto de tarefas, uma das quais é a tarefa padrão: neste caso, a tarefa denominada «compile». Um script Ant é chamado para executar uma tarefa T. Se nenhuma tarefa for especificada, a tarefa padrão é executada. basedir="." indica que, para todos os caminhos relativos encontrados no script, o ponto de partida é a pasta que contém o script Ant, neste caso a pasta <examples>/hibernate/direct/people-entities.
  • Linhas 3–11: definem variáveis do script utilizando a tag <property name="variableName" value="variableValue"/>. A variável pode então ser utilizada no script com a notação ${variableName}. Os nomes podem ser quaisquer. Vamos analisar mais de perto as variáveis definidas nas linhas 9–11:
    • Linha 9: define uma variável chamada "src.java.dir" (o nome é arbitrário) que, mais adiante no script, se referirá à pasta que contém o código-fonte Java. O seu valor é "src", um caminho relativo à pasta designada pelo atributo basedir (linha 1). Este é, portanto, o caminho "./src", onde . aqui se refere à pasta <examples>/hibernate/direct/people-entities. O código-fonte Java está, de facto, localizado na pasta <people-entities>/src (ver [6] acima).
    • Linha 10: define uma variável chamada "lib.dir" que, mais adiante no script, se referirá à pasta que contém os ficheiros JAR necessários para as tarefas Java do script. O seu valor ../../../lib refere-se à pasta <examples>/lib (ver [3] acima).
    • Linha 11: define uma variável chamada "build.dir" que, mais adiante no script, se referirá à pasta onde os ficheiros .class gerados a partir da compilação das fontes .java devem ser colocados. O seu valor "bin" refere-se à pasta <personnes-entites>/bin. Já explicámos que, no projeto Eclipse que estudámos, a pasta <bin> era onde os ficheiros .class eram gerados. O Ant fará o mesmo.
    • Linhas 14–18: A tag <path> é utilizada para definir elementos do classpath que as tarefas do Ant irão utilizar. Aqui, o caminho "project.classpath" (o nome é arbitrário) inclui todos os ficheiros .jar na árvore de diretórios <examples>/lib.
    • Linhas 21–24: A tag <patternset> é utilizada para designar um conjunto de ficheiros utilizando padrões de nomenclatura. Aqui, o patternset denominado conf refere-se a todos os ficheiros com a extensão .xml ou .properties. Este conjunto de padrões será utilizado para referir os ficheiros .xml e .properties na pasta <src> (persistence.xml, log4j.properties) (ver [6]), que são ficheiros de configuração da aplicação. Quando determinadas tarefas são executadas, estes ficheiros devem ser copiados para a pasta <bin> para que fiquem no classpath do projeto. Utilizaremos então o conjunto de padrões conf para os referenciar.
    • Linhas 27–30: A tag <target> denota uma tarefa no script. Esta é a primeira que encontramos. Tudo o que a precedeu diz respeito à configuração do ambiente de execução do script Ant. A tarefa chama-se clean. É executada em duas etapas: a pasta <bin> é eliminada (linha 28) e, em seguida, recriada (linha 29).
    • Linhas 33–35: A tarefa compile, que é a tarefa padrão do script (linha 1). Ela depende (atributo depends) da tarefa clean. Isto significa que, antes de executar a tarefa compile, o Ant deve executar a tarefa clean, ou seja, limpar a pasta <bin>. O objetivo da tarefa compile aqui é compilar os ficheiros fonte Java na pasta <src>.
    • Linha 34: Chamada ao compilador Java com três parâmetros:
      • srcdir: a pasta que contém os ficheiros fonte Java, neste caso a pasta <src>
      • destdir: a pasta onde os ficheiros .class gerados devem ser armazenados, neste caso a pasta <bin>
      • classpathref: o classpath a utilizar para a compilação, neste caso todos os ficheiros JAR na árvore de diretórios <lib>
  • (continuação)
    • linhas 38–45: a tarefa copyconf, cujo objetivo é copiar todos os ficheiros .xml e .properties do diretório <src> para o diretório <bin>.
    • linha 48: definição de uma tarefa utilizando a tag <taskdef>. Esta tarefa destina-se a ser reutilizada noutros pontos do script. Trata-se de uma conveniência de codificação. Como a tarefa é utilizada em vários pontos do script, é definida uma única vez com a tag <taskdef> e depois reutilizada através do seu nome quando necessário.
      • A tarefa chama-se hibernatetool (atributo name).
      • A sua classe é definida pelo atributo classname. Neste caso, a classe especificada encontra-se no arquivo [hibernate-tools.jar] que mencionámos anteriormente.
      • O atributo classpathref indica ao Ant onde procurar a classe anterior
  • (continuação)
    • As linhas 51–60 dizem respeito à tarefa que nos interessa aqui: gerar o esquema da base de dados para os objetos @Entity no nosso projeto Eclipse.
      • Linha 51: A tarefa chama-se DDL (abreviatura de Data Definition Language, a linguagem SQL utilizada para criar objetos de base de dados). Depende das tarefas compile e copyconf, por essa ordem. A tarefa DDL irá, portanto, acionar, por ordem, a execução das tarefas clean, compile e copyconf. Quando a tarefa DDL é iniciada, a pasta <bin> contém os ficheiros .class gerados a partir das fontes .java, nomeadamente os objetos @Entity, bem como o ficheiro [META-INF/persistence.xml] que configura a camada JPA/Hibernate.
      • Linhas 53–59: A tarefa [hibernatetool] definida na linha 48 é chamada. São-lhe passados vários parâmetros, para além dos já definidos na linha 48:
      • Linha 53: O diretório de saída para os resultados produzidos pela tarefa será o diretório atual.
      • Linha 54: O classpath da tarefa será a pasta <bin>.
      • Linha 56: indica à tarefa [hibernatetool] como determinar o seu ambiente de execução: a tag <jpaconfiguration/> indica que se encontra num ambiente JPA e que, por conseguinte, deve utilizar o ficheiro [META-INF/persistence.xml], que irá encontrar aqui no seu classpath.
      • Linha 58 define as condições para gerar a base de dados: drop=true indica que as instruções SQL drop table devem ser emitidas antes da criação das tabelas; create=true indica que o ficheiro de texto contendo as instruções SQL para criar a base de dados deve ser criado; outputfilename especifica o nome deste ficheiro SQL — aqui schema.sql na pasta <ddl> do projeto Eclipse; export=false indica que as instruções SQL geradas não devem ser executadas numa ligação ao SGBD. Este ponto é importante: significa que o SGBD de destino não precisa de estar em execução para executar a tarefa. delimiter define o caractere que separa duas instruções SQL no esquema gerado, e format=true solicita que seja aplicada uma formatação básica ao texto gerado.
  • (continuação)
    • As linhas 63–72 definem a tarefa denominada BD. É idêntica à tarefa DDL anterior, exceto que, desta vez, gera a base de dados (export="true" na linha 70). A tarefa abre uma ligação ao SGBD utilizando as informações encontradas em [persistence.xml], para executar o esquema SQL e gerar a base de dados. Para executar a tarefa BD, o SGBD deve, portanto, estar em execução.

2.1.7. Executar a tarefa DDL do ant

Para executar o script [ant-hibernate.xml], precisamos primeiro de fazer algumas configurações no Eclipse.

  • em [1]: selecione [Ferramentas Externas]
  • em [2]: crie uma nova configuração do Ant
  • em [3]: nomeie a configuração do Ant
  • Em [5]: especifique o script Ant utilizando o botão [4]
  • Passo [6]: Aplique as alterações
  • em [7]: a configuração DDL do Ant foi criada
  • em [8]: no separador JRE, defina o JRE a utilizar. O campo [10] é normalmente preenchido automaticamente com o JRE utilizado pelo Eclipse. Por isso, normalmente não há nada a fazer neste painel. No entanto, deparei-me com um caso em que o script Ant não conseguiu encontrar o compilador <javac>. Este compilador não se encontra num JRE (Java Runtime Environment), mas sim num JDK (Java Development Kit). A ferramenta Ant do Eclipse localiza este compilador através da variável de ambiente JAVA_HOME (Iniciar / Painel de Controlo / Desempenho e Manutenção / Sistema / separador Avançado / botão Variáveis de Ambiente) [A]. Se esta variável não tiver sido definida, pode permitir que o Ant encontre o compilador <javac> especificando um JDK em vez de um JRE em [10]. O JDK está disponível na mesma pasta que o JRE [B]. Utilize o botão [9] para registar o JDK entre os JREs disponíveis [C], para que possa selecioná-lo em [10].
  • Em [12]: No separador [Targets], selecione a tarefa DDL. Assim, a configuração do Ant a que chamámos DDL [7] corresponderá à execução da tarefa denominada DDL [12], que, como sabemos, gera o esquema DDL para a base de dados que representa os objetos @Entity da aplicação.
  • em [13]: valide a configuração
  • Em [14]: Execute-o

Na vista [Console], verá os registos da execução da tarefa DDL Ant:


Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
   [delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
    [mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
    [javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
     [copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
  • Lembre-se de que a tarefa DDL é denominada [hibernatetool] (linha 10) e depende das tarefas clean (linha 2), compile (linha 5) e copyconf (linha 7).
  • Linha 10: A tarefa [hibernatetool] utiliza o ficheiro [persistence.xml] de uma configuração JPA
  • linha 11: a tarefa [hbm2ddl] irá gerar o esquema DDL da base de dados
  • Linhas 12–22: o esquema DDL da base de dados

Lembre-se de que instruímos a tarefa [hbm2ddl] a gerar o esquema DDL num local específico:


<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
  • linha 74: o esquema deve ser gerado no ficheiro ddl/schema.sql. Vamos verificar:
  • em [1]: o ficheiro ddl/schema.sql está de facto presente (prima F5 para atualizar a árvore de diretórios)
  • em [2]: o seu conteúdo. Este é o esquema para uma base de dados MySQL5. O ficheiro de configuração [persistence.xml] para a camada JPA especificava, de facto, um SGBD MySQL5 (linha 8 abaixo):

 
            <!-- connexion JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
            <!--  création automatique du schéma -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecte -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  propriétés DataSource c3p0 -->
...

Vamos examinar o mapeamento objeto-relacional implementado aqui, analisando a configuração do objeto @Entity Person e o esquema DDL gerado:

Vale a pena destacar alguns pontos:

  • A1-B1: O nome da tabela especificado em A1 é, de facto, o utilizado em B1. Repare na instrução `DROP` que precede a instrução `CREATE` em B1.
  • A2-B2: mostram como a chave primária é gerada. O modo AUTO especificado em A2 resultou no atributo de autoincremento específico do MySQL 5. O modo de geração da chave primária é, na maioria das vezes, específico do SGBD.
  • A3-B3: mostram o tipo de bits SQL específico do MySQL 5 utilizado para representar um tipo booleano Java.

Vamos repetir este teste com outro SGBD:

  • A pasta [conf] [1] contém ficheiros [persistence.xml] para vários SGBDs. Pegue no ficheiro do Oracle [2], por exemplo, e coloque-o na pasta [META-INF] [3] no lugar do anterior. O seu conteúdo é o seguinte:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Persistent classes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- logs SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  automatic schematic creation -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  properties DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Recomenda-se aos leitores que consultem o apêndice, nomeadamente a secção sobre Oracle (Secção 5.7), especialmente para compreender a configuração JDBC.

Apenas a linha 25 é realmente importante aqui: estamos a indicar ao Hibernate que o SGBD é agora um SGBD Oracle. A execução da tarefa DDL do ant produz o resultado [4] apresentado acima. Note-se que o esquema Oracle difere do esquema MySQL5. Este é um ponto forte fundamental do JPA: o programador não precisa de se preocupar com estes detalhes, o que aumenta significativamente a portabilidade das suas aplicações.

2.1.8. Executar a tarefa Ant « »

Talvez se lembre que a tarefa Ant denominada BD faz o mesmo que a tarefa *DDL*, mas também gera a base de dados. O SGBD deve, portanto, estar em execução. Iremos utilizar o SGBD MySQL5 e convidamos o leitor a copiar o ficheiro [conf/mysql5/persistence.xml] para a pasta [src/META-INF]. Para verificar se a tarefa está a funcionar, iremos utilizar o plugin SQL Explorer (ver Secção 5.2.6) para verificar o estado da base de dados JPA antes e depois de executar a tarefa Ant BD.

Em primeiro lugar, precisamos de criar uma nova configuração Ant para executar a tarefa BD. Convidamos o leitor a seguir o procedimento descrito para a configuração Ant do DDL na secção 2.1.7. A nova configuração Ant terá o nome BD:

  • em [1]: duplicamos a configuração anterior denominada DDL
  • em [2]: nomeamos a nova configuração BD. Ela executa a tarefa ant BD [3], que gera fisicamente a base de dados.
  • Depois de fazer isto, inicie o SGBD MySQL5 (Secção 5.5).

Utilizamos agora o plugin SQL Explorer para explorar as bases de dados geridas pelo SGBD. O leitor deve familiarizar-se com este plugin previamente, se necessário (ver secção 5.2.6).

  • [1]: Abra a perspetiva SQL Explorer [Janela / Abrir Perspetiva / Outra]
  • [2]: Se necessário, crie uma ligação [mysql5-jpa] (consulte a secção 5.5.5, página 252) e abra-a
  • [3]: Inicie sessão como jpa / jpa
  • [4]: Está agora ligado ao MySQL5.
  • Em [5]: A base de dados jpa tem apenas uma tabela: [articles]
  • em [6]: Execute a tarefa Ant DB. Como se encontra na perspetiva [SQL Explorer], não consegue ver a vista [Console], que apresenta os registos da tarefa. Pode apresentar esta vista [Window / Show View / ...] ou regressar à perspetiva Java [Window / Open Perspective / ...].
  • em [7]: assim que a tarefa DB estiver concluída, volte à perspetiva [SQL Explorer], se necessário, e atualize a árvore da base de dados JPA.
  • Em [8]: Pode ver a tabela [jpa01_personne] que foi criada.

Os leitores são encorajados a repetir este processo de geração de base de dados com outros SGBDs. O procedimento é o seguinte:

  • Copie o ficheiro [conf/<dbms>/persistence.xml] para a pasta [src/META-INF], onde <dbms> é o SGBD que está a ser testado
  • inicie o <dbms> seguindo as instruções no apêndice para esse SGBD
  • na vista SQL Explorer, crie uma ligação ao <dbms>. Isto também é explicado nos apêndices de cada SGBD
  • Repita os testes anteriores

Nesta altura, obtivemos várias conclusões:

  • Temos uma melhor compreensão do conceito de ponte objeto-relacional. Aqui, foi implementada utilizando o Hibernate. Utilizaremos o TopLink mais tarde.
  • Sabemos que esta ponte objeto-relacional é configurada em dois locais:
  • nos objetos @Entity, onde especificamos as relações entre os campos do objeto e as colunas da tabela da base de dados
  • no [META-INF/persistence.xml], onde fornecemos à implementação JPA informações sobre os dois componentes da ponte objeto-relacional: os objetos @Entity (objeto) e a base de dados (relacional).
  • Criámos duas tarefas Ant, denominadas DDL e DB, que nos permitem criar a base de dados com base na configuração anterior, mesmo antes de escrever qualquer código Java.

Agora que a camada JPA da nossa aplicação está devidamente configurada, podemos começar a explorar a API JPA com código Java.

2.1.9. O contexto de persistência de uma aplicação

Vamos dar uma olhada mais de perto no ambiente de execução de um cliente JPA:

Sabemos que a camada JPA [2] cria uma ponte entre objetos [3] e dados relacionais [4]. O «contexto de persistência» refere-se ao conjunto de objetos geridos pela camada JPA dentro desta ponte objeto-relacional. Para aceder aos dados no contexto de persistência, um cliente JPA [1] deve passar pela camada JPA [2]:

  1. pode criar um objeto e pedir à camada JPA para o tornar persistente. O objeto passa então a fazer parte do contexto de persistência.
  2. pode solicitar uma referência a um objeto persistente existente à camada [JPA].
  3. pode modificar um objeto persistente obtido da camada JPA.
  4. pode solicitar à camada JPA que remova um objeto do contexto de persistência.

A camada JPA fornece ao cliente uma interface chamada [EntityManager] que, como o próprio nome sugere, permite a gestão de objetos @Entity no contexto de persistência. Abaixo estão os principais métodos desta interface:

void persist(Object entity)
Adiciona a entidade ao contexto de persistência
void remover(Object entidade)
remove a entidade do contexto de persistência
<T> T merge(T entity)
mescla um objeto de entidade proveniente do cliente que não é gerido pelo contexto de persistência
com o objeto de entidade no contexto de persistência que possui a mesma chave primária.
O resultado devolvido é o objeto de entidade do contexto de persistência.
<T> T find(Class<T> entityClass,
 Object primaryKey)
coloca um objeto recuperado da base de dados
através da sua chave primária. O tipo T do objeto permite
que a camada JPA saiba qual a tabela a consultar.
O objeto persistente assim criado é devolvido ao cliente.
Query createQuery(String queryText)
cria um objeto Query a partir de uma consulta JPQL
(Java Persistence Query Language). Uma consulta JPQL é análoga
a uma consulta SQL, exceto que consulta objetos em vez de tabelas.
Query createNativeQuery(String queryText)
Um método semelhante ao anterior, exceto que queryText é
uma instrução SQL em vez de uma consulta JPQL.
Query createNamedQuery(String name)
Um método idêntico ao createQuery, exceto que a consulta JPQL queryText foi
foi externalizada para um ficheiro de configuração e associada a um nome.
Este nome é o parâmetro do método.

Um objeto EntityManager tem um ciclo de vida que não é necessariamente o mesmo que o da aplicação. Tem um início e um fim. Assim, um cliente JPA pode trabalhar sucessivamente com diferentes objetos EntityManager. O contexto de persistência associado a um EntityManager tem o mesmo ciclo de vida que o próprio EntityManager. São inseparáveis um do outro. Quando um objeto EntityManager é fechado, o seu contexto de persistência é sincronizado com a base de dados, se necessário, e depois deixa de existir. Deve ser criado um novo EntityManager para obter um novo contexto de persistência.

O cliente JPA pode criar um EntityManager e, consequentemente, um contexto de persistência com a seguinte instrução:


        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  • javax.persistence.Persistence é uma classe estática utilizada para obter uma fábrica de objetos EntityManager. Esta fábrica está associada a uma unidade de persistência específica. Recorde-se que o ficheiro de configuração [META-INF/persistence.xml] é utilizado para definir unidades de persistência, cada uma das quais tem um nome:

    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">

No exemplo acima, a unidade de persistência é denominada jpa. Ela vem com a sua própria configuração específica, incluindo o sistema de gestão de bases de dados (DBMS) com o qual funciona. A instrução [Persistence.createEntityManagerFactory("jpa")] cria uma EntityManagerFactory capaz de fornecer objetos EntityManager concebidos para gerir contextos de persistência associados à unidade de persistência denominada jpa. Um objeto EntityManager — e, portanto, um contexto de persistência — é obtido a partir do objeto EntityManagerFactory da seguinte forma:

        EntityManager em = emf.createEntityManager();

Os seguintes métodos da interface [EntityManager] permitem-lhe gerir o ciclo de vida do contexto de persistência:

void close()
O contexto de persistência é fechado. Força a sincronização do contexto de persistência com a base de dados:
  • se um objeto no contexto não estiver presente na base de dados, é inserido através de uma operação SQL INSERT)
  • se um objeto no contexto estiver presente na base de dados e tiver sido modificado desde que foi lido, é executada uma operação SQL UPDATE para persistir a modificação
  • se um objeto no contexto tiver sido marcado como "eliminado" na sequência de uma operação de remoção sobre ele, é executada uma operação SQL DELETE para o remover da base de dados.
void clear()
O contexto de persistência é limpo de todos os seus objetos, mas não é fechado.
void flush()
O contexto de persistência é sincronizado com a base de dados, tal como descrito para close()

O cliente JPA pode forçar a sincronização do contexto de persistência com a base de dados utilizando o método [EntityManager].flush. A sincronização pode ser explícita ou implícita. No primeiro caso, cabe ao cliente realizar operações de flush quando quiser sincronizar; caso contrário, a sincronização ocorre em momentos específicos que iremos especificar. O modo de sincronização é gerido pelos seguintes métodos da interface [EntityManager]:

void setFlushMode(FlushModeType flushMode)
Existem dois valores possíveis para flushMode:
FlushModeType.AUTO (padrão): a sincronização ocorre antes de cada consulta SELECT efetuada na base de dados.
FlushModeType.COMMIT: a sincronização ocorre apenas no final das transações da base de dados.
FlushModeType getFlushMode()
retorna o modo de sincronização atual

Vamos resumir. No modo FlushModeType.AUTO, que é o padrão, o contexto de persistência será sincronizado com a base de dados nos seguintes momentos:

  1. antes de cada operação SELECT no banco de dados
  2. no final de uma transação na base de dados
  3. após uma operação de flush ou de fecho no contexto de persistência

No modo FlushModeType.COMMIT, o mesmo se aplica, exceto para a operação 1, que não ocorre. O modo normal de interação com a camada JPA é o modo transacional. O cliente realiza várias operações no contexto de persistência dentro de uma transação. Neste caso, os pontos de sincronização entre o contexto de persistência e a base de dados são os casos 1 e 2 acima no modo AUTO, e apenas o caso 2 no modo COMMIT.

Concluamos com a API da interface Query, que permite emitir comandos JPQL no contexto de persistência ou comandos SQL diretamente na base de dados para recuperar dados. A interface Query é a seguinte:

Iremos utilizar os métodos 1 a 4 acima:

  • 1 - O método getResultList executa uma consulta SELECT que devolve vários objetos. Estes são devolvidos num objeto List. Este objeto é uma interface. Fornece um objeto Iterator que permite percorrer os elementos da lista L da seguinte forma:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
            // exploiter l'objet iterator.next() qui représente l'élément courant de la liste
...
}

A lista L também pode ser percorrida utilizando um ciclo for:


        for (Object o : L) {
            // exploiter objet o
}
  • 2 - O método `getSingleResult` executa uma instrução JPQL/SQL SELECT que devolve um único objeto.
  • 3 - O método `executeUpdate` executa uma instrução SQL UPDATE ou DELETE e devolve o número de linhas afetadas pela operação.
  • 4 - O método setParameter(String, Object) permite atribuir um valor a um parâmetro nomeado numa consulta JPQL parametrizada.
  • 5 - O método setParameter(int, Object) define o parâmetro, mas este é identificado não pelo seu nome, mas pela sua posição na consulta JPQL.

2.1.10. Um primeiro cliente JPA

Voltemos à perspetiva Java do projeto:

 

Agora sabemos quase tudo sobre este projeto, exceto o conteúdo da pasta [src/tests], que iremos examinar a seguir. A pasta contém dois programas de teste para a camada JPA:

  • [InitDB.java] é um programa que insere algumas linhas na tabela [jpa01_personne] da base de dados. O seu código irá apresentar-nos os primeiros elementos da camada JPA.
  • [Main.java] é um programa que realiza operações CRUD na tabela [jpa01_personne]. O estudo do seu código permitir-nos-á explorar os conceitos fundamentais do contexto de persistência e do ciclo de vida dos objetos dentro desse contexto.

2.1.10.1. O código

O código do programa [InitDB.java] é o seguinte:


package tests;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
 
import entites.Personne;
 
public class InitDB {
    // constant
    private final static String TABLE_NAME = "jpa01_personne";
 
    public static void main(String[] args) throws ParseException {
        // Persistence unit
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        // retrieve a EntityManagerFactory from the persistence unit
        EntityManager em = emf.createEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // delete items from the people table
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // create two people
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // persistence of people
        em.persist(p1);
        em.persist(p2);
        // people display
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // end transaction
        tx.commit();
        // end EntityManager
        em.close();
        // end EntityManagerFactory
        emf.close();
        // log
        System.out.println("terminé ...");
    }
}

Este código deve ser lido à luz do que foi explicado na secção 2.1.9.

  • Linha 19: É solicitado um objeto EntityManagerFactory (emf) para a unidade de persistência JPA (definida em persistence.xml). Esta operação é normalmente realizada apenas uma vez durante o ciclo de vida de uma aplicação.
  • Linha 21: é solicitado um objeto EntityManager (em) para gerir um contexto de persistência.
  • Linha 23: é solicitado um objeto Transaction para gerir uma transação. Note-se que as operações no contexto de persistência devem ser realizadas dentro de uma transação. Veremos que isto não é estritamente necessário, mas não o fazer pode causar problemas. Se a aplicação for executada num contentor EJB3, as operações no contexto de persistência são sempre realizadas dentro de uma transação.
  • Linha 24: A transação começa
  • linha 26: executa uma instrução SQL de eliminação na tabela «jpa01_personne» (nativeQuery). Fazemos isto para limpar a tabela de todo o conteúdo e, assim, ver melhor o resultado da execução da aplicação [InitDB]
  • Linhas 28–29: São criados dois objetos da classe `Person`, p1 e p2. Trata-se de objetos comuns e, por enquanto, não têm qualquer relação com o contexto de persistência. No que diz respeito ao contexto de persistência, o Hibernate refere-se a estes objetos como estando num estado transitório, em oposição aos objetos persistentes, que são geridos pelo contexto de persistência. Em vez disso, referir-nos-emos a objetos não persistentes (um termo não padronizado) para indicar que ainda não são geridos pelo contexto de persistência, e a objetos persistentes para aqueles que são geridos por ele. Encontraremos uma terceira categoria de objetos: objetos desligados, que são objetos que eram anteriormente persistentes, mas cujo contexto de persistência foi encerrado. O cliente pode manter referências a tais objetos, o que explica por que não são necessariamente destruídos quando o contexto de persistência é encerrado. Diz-se então que se encontram num estado desligado. A operação [EntityManager].merge permite que sejam novamente ligados a um contexto de persistência recém-criado.
  • Linhas 31–32: As entidades p1 e p2 são adicionadas ao contexto de persistência através da operação [EntityManager].persist. Tornam-se então objetos persistentes.
  • Linhas 35–37: É executada uma consulta JPQL “select p from Person p order by p.name asc”. Person não é a tabela (que se chama jpa01_person), mas o objeto @Entity associado à tabela. Aqui temos uma consulta JPQL (Java Persistence Query Language) no contexto de persistência, não uma consulta SQL na base de dados. Dito isto, à exceção do objeto Person que substituiu a tabela jpa01_personne, as sintaxes são idênticas. Um loop for percorre a lista (de pessoas) resultante do select para apresentar cada elemento na consola. Aqui, estamos a verificar se os elementos colocados no contexto de persistência nas linhas 31–32 estão de facto presentes na tabela. Ocorrerá uma sincronização transparente do contexto de persistência com a base de dados. De facto, será emitida uma consulta SELECT, e observámos que este é um dos casos em que a sincronização ocorre. É, portanto, neste momento que, em segundo plano, o JPA/Hibernate emitirá as duas instruções SQL INSERT que irão inserir as duas pessoas na tabela jpa01_personne. A operação `persist` não fez isto. Esta operação adiciona objetos ao contexto de persistência sem afetar a base de dados. O trabalho efetivo ocorre durante a sincronização, neste caso imediatamente antes da consulta `SELECT` na base de dados.
  • Linha 39: Encerramos a transação iniciada na linha 24. Uma sincronização ocorrerá novamente. Nada acontecerá aqui, uma vez que o contexto de persistência não sofreu alterações desde a última sincronização.
  • Linha 41: Encerramos o contexto de persistência.
  • Linha 43: Encerramos a fábrica do EntityManager.

2.1.10.2. O : execução do código

  • Inicie o SGBD MySQL5
  • Coloque conf/mysql5/persistence.xml em META-INF/persistence.xml, se necessário
  • Execute a aplicação [InitDB]

São obtidos os seguintes resultados:

  • em [1]: a saída da consola na perspetiva Java. São obtidos os resultados esperados.
  • em [2]: verificamos o conteúdo da tabela [jpa01_personne] utilizando a vista do SQL Explorer, conforme explicado na secção 2.1.8. Há dois pontos que merecem destaque:
    • o ID da chave primária foi gerado automaticamente
    • o mesmo se aplica ao número de versão. Vemos que a primeira versão tem o número 0..

Aqui temos os primeiros elementos da estrutura JPA. Conseguimos inserir dados numa tabela. Vamos partir desta base para escrever o segundo teste, mas primeiro vamos discutir os registos.

2.1.11. Implementação dos registos do Hibernate

É possível visualizar as instruções SQL enviadas para a base de dados pela camada JPA/Hibernate. É útil examiná-las para verificar se a camada JPA é tão eficiente quanto um programador que tivesse escrito as instruções SQL por conta própria.

Com o JPA/Hibernate, o registo de SQL pode ser configurado no ficheiro [persistence.xml]:


            <!-- Classes persistantes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- logs SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- connexion JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
 
  • Linhas 4–6: Os registos SQL não estavam ativados nesta altura. Ativamo-los agora removendo as tags de comentário das linhas 3 e 7.

Executamos novamente a aplicação [InitDB]. A saída da consola fica então da seguinte forma:

Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
[personnes]
Hibernate: 
    select
        personne0_.ID as ID0_,
        personne0_.VERSION as VERSION0_,
        personne0_.NOM as NOM0_,
        personne0_.PRENOM as PRENOM0_,
        personne0_.DATENAISSANCE as DATENAIS5_0_,
        personne0_.MARIE as MARIE0_,
        personne0_.NBENFANTS as NBENFANTS0_ 
    from
        jpa01_personne personne0_ 
    order by
        personne0_.NOM asc
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
terminé ...
  • Linhas 2-4: A instrução SQL DELETE resultante do comando:

        // supprimer les éléments de la table des personnes
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
  • linhas 5-18: as instruções SQL INSERT das instruções:

        // persistance des personnes
        em.persist(p1);
        em.persist(p2);
  • linhas 21-32: a instrução SQL SELECT resultante da instrução:

        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) 

Se realizarmos impressões intermédias na consola, veremos que os registos SQL relativos à instrução I no código Java são gravados quando a instrução I é executada. Isto não significa que a instrução SQL apresentada seja executada na base de dados nesse momento. Na verdade, ela é armazenada em cache para ser executada durante a próxima sincronização do contexto de persistência com a base de dados.

É possível obter registos adicionais através do ficheiro [src/log4j.properties]:

  • Em [1], o ficheiro [log4j.properties] é utilizado pelo arquivo [log4j-1.2.13.jar] [2] da ferramenta chamada LOG4j (Logs for Java), disponível no URL [http://logging.apache.org/log4j/docs/index.html]. Colocado na pasta [src] do projeto Eclipse, sabemos que o [log4j.properties] será automaticamente copiado para a pasta [bin] do projeto [3]. Uma vez feito isso, ele passa a estar no classpath do projeto, e é aí que o arquivo [2] irá recuperá-lo.

O ficheiro [log4j.properties] permite-nos controlar determinados registos do Hibernate. Em execuções anteriores, o seu conteúdo era o seguinte:


# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
 
# Root logger option
log4j.rootLogger=ERROR, stdout
 
# Hibernate logging options (INFO only shows startup messages)
#log4j.logger.org.hibernate=INFO
 
# Log JDBC bind parameter runtime arguments
#log4j.logger.org.hibernate.type=DEBUG

Não vou comentar muito sobre esta configuração, uma vez que nunca dediquei tempo a aprender a sério sobre o LOG4j.

  • As linhas 1–8 encontram-se em todos os ficheiros log4j.properties que encontrei
  • As linhas 10–14 estão presentes nos ficheiros log4j.properties dos exemplos do Hibernate.
  • Linha 11: controla os registos gerais do Hibernate. Como a linha está comentada, estes registos estão desativados aqui. Existem vários níveis de registo: INFO (informação geral sobre o que o Hibernate está a fazer), WARN (o Hibernate avisa-nos de um potencial problema), DEBUG (registos detalhados). O nível INFO é o menos detalhado, enquanto o modo DEBUG é o mais detalhado. Ativar a linha 11 permite-lhe ver o que o Hibernate está a fazer, particularmente quando a aplicação é iniciada. Isto é frequentemente útil.
  • A linha 12, se ativada, permite-lhe ver os argumentos reais utilizados ao executar consultas SQL parametrizadas.

Vamos começar por descomentar a linha 14


# Log JDBC bind parameter runtime arguments
log4j.logger.org.hibernate.type=DEBUG

e execute novamente [InitDB]. Os novos registos gerados por esta alteração são os seguintes (visualização parcial):

Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 1
07:20:03,843 DEBUG StringType:80 - binding 'Durant' to parameter: 2
07:20:03,843 DEBUG StringType:80 - binding 'Sylvie' to parameter: 3
07:20:03,843 DEBUG DateType:80 - binding '05 juillet 2001' to parameter: 4
07:20:03,843 DEBUG BooleanType:80 - binding 'false' to parameter: 5
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 6
  • As linhas 8–10 são novos registos gerados ao ativar a linha 14 do ficheiro [log4j.properties]. Indicam os 5 valores atribuídos aos parâmetros formais ? da consulta parametrizada nas linhas 2–7. Assim, vemos que a coluna VERSION receberá o valor 0 (linha 8).

Agora, vamos ativar a linha 11 do [log4j.properties]:

# Hibernate logging options (INFO only shows startup messages)
log4j.logger.org.hibernate=INFO

e execute novamente [InitDB]:

07:50:23,937  INFO Version:15 - Hibernate EntityManager 3.2.0.CR3
07:50:23,968  INFO Version:15 - Hibernate Annotations 3.2.0.CR3
07:50:23,984  INFO Environment:500 - Hibernate 3.2.0.cr5
07:50:23,984  INFO Environment:533 - hibernate.properties not found
07:50:23,984  INFO Environment:667 - Bytecode provider name : cglib
07:50:24,000  INFO Environment:584 - using JDK 1.4 java.sql.Timestamp handling
07:50:24,375  INFO AnnotationBinder:387 - Binding entity from annotated class: entites.Personne
07:50:24,421  INFO EntityBinder:340 - Bind entity entites.Personne on table jpa01_personne
07:50:24,609  INFO C3P0ConnectionProvider:50 - C3P0 using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost:3306/jpa
07:50:24,609  INFO C3P0ConnectionProvider:51 - Connection properties: {user=jpa, password=****, autocommit=true, release_mode=auto}
07:50:24,609  INFO C3P0ConnectionProvider:54 - autocommit mode: true
07:50:25,296  INFO SettingsFactory:81 - RDBMS: MySQL, version: 5.0.37-community-nt
07:50:25,296  INFO SettingsFactory:82 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
07:50:25,312  INFO Dialect:141 - Using dialect: org.hibernate.dialect.MySQL5InnoDBDialect
07:50:25,312  INFO TransactionFactoryFactory:34 - Transaction strategy: org.hibernate.transaction.JDBCTransactionFactory
07:50:25,312  INFO TransactionManagerLookupFactory:33 - No TransactionManagerLookup configured (in JTA environment, use of read-write or transactional second-level cache is not recommended)
07:50:25,328  INFO SettingsFactory:134 - Automatic flush during beforeCompletion(): disabled
07:50:25,328  INFO SettingsFactory:138 - Automatic session close at end of transaction: disabled
07:50:25,328  INFO SettingsFactory:145 - JDBC batch size: 15
07:50:25,328  INFO SettingsFactory:148 - JDBC batch updates for versioned data: disabled
07:50:25,328  INFO SettingsFactory:153 - Scrollable result sets: enabled
07:50:25,328  INFO SettingsFactory:161 - JDBC3 getGeneratedKeys(): enabled
07:50:25,328  INFO SettingsFactory:169 - Connection release mode: auto
07:50:25,328  INFO SettingsFactory:193 - Maximum outer join fetch depth: 2
07:50:25,328  INFO SettingsFactory:196 - Default batch fetch size: 1
07:50:25,328  INFO SettingsFactory:200 - Generate SQL with comments: disabled
07:50:25,328  INFO SettingsFactory:204 - Order SQL updates by primary key: disabled
07:50:25,328  INFO SettingsFactory:369 - Query translator: org.hibernate.hql.ast.ASTQueryTranslatorFactory
07:50:25,328  INFO ASTQueryTranslatorFactory:24 - Using ASTQueryTranslatorFactory
07:50:25,328  INFO SettingsFactory:212 - Query language substitutions: {}
07:50:25,328  INFO SettingsFactory:217 - JPA-QL strict compliance: enabled
07:50:25,328  INFO SettingsFactory:222 - Second-level cache: enabled
07:50:25,328  INFO SettingsFactory:226 - Query cache: disabled
07:50:25,328  INFO SettingsFactory:356 - Cache provider: org.hibernate.cache.NoCacheProvider
07:50:25,328  INFO SettingsFactory:241 - Optimize cache for minimal puts: disabled
07:50:25,328  INFO SettingsFactory:250 - Structured second-level cache entries: disabled
07:50:25,343  INFO SettingsFactory:270 - Echoing all SQL to stdout
07:50:25,343  INFO SettingsFactory:277 - Statistics: disabled
07:50:25,343  INFO SettingsFactory:281 - Deleted entity synthetic identifier rollback: disabled
07:50:25,343  INFO SettingsFactory:296 - Default entity-mode: pojo
07:50:25,468  INFO SessionFactoryImpl:161 - building session factory
07:50:25,750  INFO SessionFactoryObjectFactory:82 - Not binding factory to JNDI, no JNDI name configured
07:50:25,765  INFO SchemaExport:154 - Running hbm2ddl schema export
07:50:25,765  INFO SchemaExport:179 - exporting generated schema to database
07:50:25,968  INFO SchemaExport:196 - schema export complete
Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    ... 

A leitura destes registos fornece muita informação interessante:

  • linha 7: o Hibernate indica o nome de uma classe @Entity que encontrou
  • linha 8: indica que a classe [Person] será mapeada para a tabela [jpa01_person]
  • linha 9: indica o pool de conexões C3P0 que será utilizado, o nome do driver JDBC e a URL da base de dados a ser gerida
  • linha 10: fornece detalhes adicionais sobre a ligação JDBC: proprietário, tipo de commit, etc.
  • linha 14: o dialeto utilizado para comunicar com o SGBD
  • linha 15: o tipo de transação utilizado. JDBCTransactionFactory indica que a aplicação gere as suas próprias transações. Não é executada num contentor EJB3 que forneceria o seu próprio serviço de transações.
  • As linhas seguintes referem-se a opções de configuração do Hibernate que ainda não abordámos. Recomenda-se aos leitores interessados que consultem a documentação do Hibernate.
  • Linha 37: as instruções SQL serão exibidas na consola. Isto foi solicitado em [persistence.xml]:

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="use_sql_comments" value="true" />
  • Linhas 43–45: O esquema da base de dados é exportado para o SGBD, ou seja, a base de dados é esvaziada e, em seguida, recriada. Este mecanismo decorre da configuração em [persistence.xml] (linha 4 abaixo):

            ...
            <property name="hibernate.connection.password" value="jpa" />
            <!--  création automatique du schéma -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecte -->
            ...

Quando uma aplicação «trava» com uma exceção do Hibernate que não compreende, comece por ativar os registos do Hibernate no modo DEBUG no ficheiro [log4j.properties] para obter uma visão mais clara:


# Root logger option
log4j.rootLogger=ERROR, stdout
 
# Hibernate logging options (INFO only shows startup messages)
log4j.logger.org.hibernate=DEBUG

No restante deste documento, o registo está desativado por predefinição para garantir uma saída de consola mais legível.

2.1.12. Explorar a linguagem de consulta JPQL/HQL com a consola do Hibernate

Nota: Esta secção requer o plugin Hibernate Tools (secção 5.2.5).

No código da aplicação [InitDB], utilizámos uma consulta JPQL. JPQL (Java Persistence Query Language) é uma linguagem para consultar o contexto de persistência. A consulta utilizada foi a seguinte:

select p from Personne p order by p.nom asc

Selecionou todos os registos da tabela associada à @Entity [Person] e devolveu-os por ordem crescente de nome. Na consulta acima, p.name é o campo de nome de uma instância p da classe [Person]. Uma consulta JPQL opera, portanto, sobre os objetos @Entity no contexto de persistência e não diretamente nas tabelas da base de dados. A camada JPA traduz esta consulta JPQL numa consulta SQL adequada ao SGBD com o qual está a trabalhar. Assim, no caso de uma implementação JPA/Hibernate ligada a um SGBD MySQL5, a consulta JPQL anterior é traduzida na seguinte consulta SQL:

select
  personne0_.ID as ID0_,
  personne0_.VERSION as VERSION0_,
  personne0_.NOM as NOM0_,
  personne0_.PRENOM as PRENOM0_,
  personne0_.DATENAISSANCE as DATENAIS5_0_,
  personne0_.MARIE as MARIE0_,
  personne0_.NBENFANTS as NBENFANTS0_ 
 from
  jpa01_personne personne0_ 
 order by
  personne0_.NOM asc

A camada JPA utilizou a configuração do objeto @Entity [Person] para gerar a consulta SQL correta. Este é um exemplo da implementação do mapeamento objeto-relacional neste contexto.

O plugin [Hibernate Tools] (Secção 5.2.5) oferece uma ferramenta chamada «Hibernate Console» que permite

  • emitir consultas JPQL ou HQL (Hibernate Query Language) no contexto de persistência
  • para recuperar os resultados
  • ver o equivalente SQL que foi executado na base de dados

O Hibernate Console é uma ferramenta inestimável para aprender a linguagem JPQL e familiarizar-se com a ponte JPQL/SQL. É sabido que a JPA se baseou fortemente em ferramentas ORM, como o Hibernate ou o TopLink. O JPQL é muito semelhante ao HQL do Hibernate, mas não inclui todas as suas funcionalidades. Na Consola do Hibernate, pode emitir comandos HQL que serão executados normalmente na consola, mas que não fazem parte da linguagem JPQL e, por isso, não podem ser utilizados num cliente JPA. Quando for esse o caso, iremos salientá-lo.

Vamos criar uma consola do Hibernate para o nosso projeto Eclipse atual:

  • [1]: Mude para a perspetiva [Hibernate Console] (Janela / Abrir Perspetiva / Outra)
  • [2]: Criamos uma nova configuração na janela [Hibernate Configuration]
  • usando o botão [4], selecionamos o projeto Java para o qual a configuração do Hibernate está a ser criada. O seu nome aparece em [3].
  • Em [5], introduzimos o nome que pretendemos para esta configuração. Aqui, utilizámos [3].
  • Em [6], especificamos que estamos a utilizar uma configuração JPA para que a ferramenta saiba que deve utilizar o ficheiro [META-INF/persistence.xml]
  • Em [7], especificamos que, neste ficheiro [META-INF/persistence.xml], deve ser utilizada a unidade de persistência denominada jpa.
  • Em [8], validamos a configuração.

Em seguida, o SGBD deve ser iniciado. Aqui, estamos a utilizar o MySQL 5.

  • Em [1]: A configuração criada apresenta uma árvore com três ramos
  • Em [2]: O ramo [Configuration] lista os objetos que a consola utilizou para se configurar: neste caso, o @Entity Person.
  • Em [3]: A Session Factory é um conceito do Hibernate semelhante ao EntityManager do JPA. Ela faz a ponte entre o mundo dos objetos e o mundo relacional utilizando os objetos no ramo [Configuration]. Em [3], são mostrados os objetos do contexto de persistência; aqui, mais uma vez, o @Entity Person.
  • em [4]: a base de dados acedida através da configuração encontrada em [persistence.xml]. A tabela [jpa01_personne] encontra-se aí.
  • Em [1], criamos um editor HQL
  • no editor HQL,
    • em [2], selecionamos a configuração do Hibernate a utilizar, caso existam várias
    • em [3], digitamos o comando JPQL que queremos executar
    • em [4], executamo-lo
  • Em [5], obtém os resultados da consulta na janela [Hibernate Query Result]. Pode deparar-se com duas situações aqui:
    • Não obtém nada (nenhuma linha). A consola do Hibernate utilizou o conteúdo de [persistence.xml] para estabelecer uma ligação com o SGBD. No entanto, esta configuração tem uma propriedade que instrui o banco de dados a ser esvaziado:

            <property name="hibernate.hbm2ddl.auto" value="create" />

Por isso, deve voltar a executar a aplicação [InitDB] antes de voltar a executar o comando JPQL acima.

  • (continuação)
    • A janela [Hibernate Query Result] não é apresentada. Pode abri-la através de [Window / Show View / ...]

A janela [Hibernate Dynamic SQL preview] ([1] abaixo) permite-lhe ver a consulta SQL que será executada para executar o comando JPQL que está a escrever neste momento. Assim que a sintaxe do comando JPQL estiver correta, o comando SQL correspondente aparece nesta janela:

  • Em [2], pode apagar o comando HQL anterior
  • Em [3], executa-se um novo
  • em [4], o resultado
  • em [5], o comando SQL que foi executado na base de dados

O editor HQL fornece assistência para escrever comandos HQL:

  • em [1]: assim que o editor reconhece que p é um objeto Person, pode sugerir os campos de p à medida que escreve.
  • em [2]: uma consulta HQL incorreta. Deve escrever where p.marie=true.
  • em [3]: o erro é relatado na janela [Pré-visualização SQL]

Convidamos o leitor a emitir outros comandos HQL/JPQL na base de dados.

2.1.13. Um segundo cliente JPA

Voltemos à perspetiva Java do projeto:

 
  • [InitDB.java] é um programa que inseriu algumas linhas na tabela [jpa01_personne] da base de dados. O estudo do seu código permitiu-nos compreender os conceitos básicos da API JPA.
  • [Main.java] é um programa que realiza operações CRUD na tabela [jpa01_personne]. A análise do seu código permitir-nos-á revisitar os conceitos fundamentais do contexto de persistência e do ciclo de vida dos objetos dentro desse contexto.

2.1.13.1. A estrutura do código

[Main.java] irá executar uma série de testes, cada um concebido para demonstrar um aspeto específico do JPA:

 

O método [main]

  • chama sucessivamente os métodos test1 a test11. Apresentaremos o código de cada um desses métodos separadamente.
  • Também utiliza métodos utilitários privados: clean, dump, log, getEntityManager, getNewEntityManager.

Apresentamos o método principal e os chamados métodos utilitários:


package tests;
 
...
import entites.Personne;
 
@SuppressWarnings("unchecked")
public class Main {
 
    // constant
    private final static String TABLE_NAME = "jpa01_personne";
 
    // Persistence context
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
    private static EntityManager em = null;
 
    // shared objects
    private static Personne p1, p2, newp1;
 
    public static void main(String[] args) throws Exception {
        // base cleaning
        log("clean");clean();
 
        // dump table
        dump();
 
        // test1
        log("test1");test1();
 
...
        // test11
        log("test11");test11();
 
        // fine persistence context
        if (em.isOpen())
            em.close();
 
        // closure EntityManagerFactory
        emf.close();
    }
 
    // retrieve the current EntityManager
    private static EntityManager getEntityManager() {
        if (em == null || !em.isOpen()) {
            em = emf.createEntityManager();
        }
        return em;
    }

    // pick up a new EntityManager
    private static EntityManager getNewEntityManager() {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = emf.createEntityManager();
        return em;
    }
 
    // table content display
    private static void dump() {
        // current persistence context
        EntityManager em = getEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // people display
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // end transaction
        tx.commit();
    }
 
    // raz BD
    private static void clean() {
        // persistence context
        EntityManager em = getEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // delete elements from the PERSONNES table
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // end transaction
        tx.commit();
    }
 
    // logs
    private static void log(String message) {
        System.out.println("main : ----------- " + message);
    }
 
    // object creation
    public static void test1() throws ParseException {
...
    }
 
    // modify a context object
    public static void test2() {
...
    }
 
    // request items
    public static void test3() {
...
    }
 
    // delete an object belonging to the persistence context
    public static void test4() {
....
    }
 
    // detach, reattach and modify
    public static void test5() {
...
    }
 
    // delete an object not belonging to the persistence context
    public static void test6() {
...
    }
 
    // modify an object not belonging to the persistence context
    public static void test7() {
...
    }
 
    // reattach an object to the persistence context
    public static void test8() {
...
    }
 
    // a select request causes synchronization
    // with the persistence context
    public static void test9() {
....
    }
 
    // version control (optimistic locking)
    public static void test10() {
...
    }
 
    // transaction rollback
    public static void test11() throws ParseException {
...
    }
 
}
  • Linha 13: O objeto EntityManagerFactory (emf) é construído a partir da unidade de persistência JPA definida em [persistence.xml]. Isso permitirá-nos criar vários contextos de persistência ao longo da aplicação.
  • linha 14: um contexto de persistência EntityManager que ainda não foi inicializado
  • linha 17: três objetos [Person] partilhados pelos testes
  • Linha 21: A tabela jpa01_personne é esvaziada e, em seguida, exibida na linha 24 para garantir que estamos a começar com uma tabela vazia.
  • Linhas 27–31: sequência de testes
  • Linhas 34–35: Fechar o contexto de persistência, caso este estivesse aberto.
  • Linha 38: O objeto EntityManagerFactory emf é fechado.
  • Linhas 42–47: O método [getEntityManager] devolve o EntityManager atual (ou contexto de persistência) ou cria um novo caso não exista (linhas 43–44).
  • Linhas 50-56: o método [getNewEntityManager] devolve um novo contexto de persistência. Se já existisse um anteriormente, este é fechado (linhas 51-52)
  • linhas 59-72: o método [dump] exibe o conteúdo da tabela [jpa01_personne]. Este código já foi encontrado em [InitDB].
  • linhas 75-85: o método [clean] esvazia a tabela [jpa01_personne]. Este código já foi visto em [InitDB].
  • Linhas 88–90: O método [log] exibe a mensagem que lhe foi passada como parâmetro na consola, para que seja notada.

Podemos agora passar ao estudo dos testes.

2.1.13.2. Teste 1

O código para o teste 1 é o seguinte:


// création d'objets
    public static void test1() throws ParseException {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // création personnes
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistance des personnes
        em.persist(p1);
        em.persist(p2);
        // fin transaction
        tx.commit();
        // on affiche la table
        dump();
 
}

Este código já foi visto em [InitDB]: cria duas pessoas e coloca-as no contexto de persistência.

  • linha 4: recuperamos o contexto de persistência atual
  • linhas 6-7: criamos as duas pessoas
  • linhas 9–15: as duas pessoas são colocadas no contexto de persistência dentro de uma transação
  • linha 15: como a transação é confirmada, o contexto de persistência é sincronizado com a base de dados. As duas pessoas serão adicionadas à tabela [jpa01_personne].
  • Linha 17: A tabela é apresentada

A saída da consola para este primeiro teste é a seguinte:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]

2.1.13.3. Teste 2

O código para o teste 2 é o seguinte:


// modifier un objet du contexte
    public static void test2() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on incrémente le nbre d'enfants de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // on modifie son état marital
        p1.setMarie(false);
        // l'objet p1 est automatiquement sauvegardé (dirty checking)
        // lors de la prochaine synchronisation (commit ou select)
        // fin transaction
        tx.commit();
        // on affiche la nouvelle table
        dump();
    }
  • O Teste 2 tem como objetivo modificar um objeto no contexto de persistência e, em seguida, exibir o conteúdo da tabela para verificar se a modificação ocorreu
  • Linha 4: Recuperar o contexto de persistência atual
  • Linhas 6–7: As operações serão realizadas dentro de uma transação
  • Linhas 9 e 11: O número de filhos da pessoa p1 é alterado, assim como o seu estado civil
  • Linha 15: Fim da transação, pelo que o contexto de persistência é sincronizado com a base de dados
  • Linha 17: exibir tabela

A saída da consola para o Teste 2 é a seguinte:

1
2
3
4
5
6
7
8
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
  • linha 4: pessoa p1 antes da modificação
  • linha 8: pessoa p1 após a modificação. Note-se que o número da versão mudou para 1. Este número é incrementado em 1 cada vez que a linha é atualizada.

2.1.13.4. Teste 3

O código para o Teste 3 é o seguinte:


    // demander des objets
    public static void test3() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on demande la personne p1
        Personne p1b = em.find(Personne.class, p1.getId());
        // parce que p1 est déjà dans le contexte de persistance, il n'y a pas eu d'accès à la base
        // p1b et p1 sont les mêmes références
        System.out.format("p1==p1b ? %s%n", p1 == p1b);
        // demander un objet qui n'existe pas rend 1 pointeur null
        Personne px = em.find(Personne.class, -4);
        System.out.format("px==null ? %s%n", px == null);
        // fin transaction
        tx.commit();
}
  • O Teste 3 centra-se no método [EntityManager.find], que recupera um objeto da base de dados e o coloca no contexto de persistência. Não iremos explicar a transação que ocorre em todos os testes, a menos que seja utilizada de forma invulgar.
  • Linha 9: Solicitamos ao contexto de persistência a pessoa com a mesma chave primária que a pessoa p1. Existem dois casos:
    • p1 já se encontra no contexto de persistência. É este o caso aqui. Por conseguinte, não é efetuado qualquer acesso à base de dados. O método find devolve simplesmente uma referência ao objeto persistido.
    • p1 não está no contexto de persistência. Neste caso, é realizada uma consulta à base de dados utilizando a chave primária fornecida. O registo recuperado é adicionado ao contexto de persistência e o método find devolve uma referência a este novo objeto persistido.
  • Linha 12: Verificamos que `find` devolveu a referência ao objeto `p1` já presente no contexto
  • Linha 14: Solicitamos um objeto que não existe nem no contexto de persistência nem na base de dados. O método find devolve então um ponteiro nulo. Isto é verificado na linha 15.

A saída da consola para o Teste 3 é a seguinte:

1
2
3
main : ----------- test3
p1==p1b ? true
px==null ? true

2.1.13.5. Teste 4

O código para o teste 4 é o seguinte:


    // supprimer un objet appartenant au contexte de persistance
    public static void test4() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on supprime l'objet persisté p2
        em.remove(p2);
        // fin transaction
        tx.commit();
        // on affiche la nouvelle table
        dump();
}
  • O Teste 4 centra-se no método [EntityManager.remove], que permite remover um elemento do contexto de persistência e, consequentemente, da base de dados.
  • linha 9: a pessoa p2 é removida do contexto de persistência
  • linha 11: sincronizar o contexto com a base de dados
  • Linha 13: Exibição da tabela. Normalmente, a pessoa p2 já não deveria estar lá.

A saída da consola para o Teste 4 é a seguinte:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test3
p1==p1b ? true
px==null ? true
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
  • linha 3: pessoa p2 em test1
  • linhas 12-14: já não existem após o teste4.

2.1.13.6. Teste 5

O código para o teste 5 é o seguinte:


// détacher, réattacher et modifier
    public static void test5() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 détaché
        Personne oldp1=p1;
        // on réattache p1 au nouveau contexte
        p1 = em.find(Personne.class, p1.getId());
        // vérification
        System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);        
        // fin transaction
        tx.commit();
        // on incrémente le nbre d'enfants de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // on affiche la nouvelle table
        dump();
    }
  • O Teste 5 examina o ciclo de vida de objetos persistentes em vários contextos de persistência sucessivos. Até agora, tínhamos sempre utilizado o mesmo contexto de persistência nos vários testes.
  • Linha 4: É solicitado um novo contexto de persistência. O método [getNewEntityManager] fecha o anterior e abre um novo. Como resultado, os objetos p1 e p2 mantidos pela aplicação já não se encontram num estado persistente. Pertenciam a um contexto que foi fechado. Dizemos que se encontram num estado desligado. Não pertencem ao novo contexto de persistência.
  • Linhas 6–7: Início da transação. Aqui, ela será utilizada de uma forma invulgar.
  • Linha 9: Anotamos o endereço do objeto p1, agora desligado.
  • Linha 11: O contexto de persistência é consultado para a pessoa p1 (usando a chave primária de p1). Como o contexto é novo, a pessoa p1 não está presente nele. Será, portanto, realizada uma consulta à base de dados. O objeto recuperado será colocado no novo contexto.
  • Linha 13: Verificamos que o objeto persistente p1 no contexto é diferente do objeto oldp1, que era o antigo objeto p1 desanexado.
  • Linha 15: A transação é concluída
  • Linha 17: Modificamos o novo objeto persistido p1 fora da transação. O que acontece neste caso? Queremos saber.
  • Linha 19: Solicitamos que a tabela seja exibida. Note que, devido à instrução `SELECT` emitida pelo método `dump`, o contexto de persistência é automaticamente sincronizado com a base de dados.

A saída da consola para o Teste 5 é a seguinte:

1
2
3
4
5
6
7
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • linha 5: o método find acedeu efetivamente à base de dados; caso contrário, os dois ponteiros seriam iguais
  • Linhas 7 e 3: O número de filhos de p1 aumentou efetivamente em 1. A modificação, efetuada fora de uma transação, foi, portanto, tida em conta. Na verdade, isto depende do SGBD utilizado. Num SGBD, uma instrução SQL é sempre executada dentro de uma transação. Se o cliente JPA não iniciar ele próprio uma transação explícita, o SGBD iniciará uma transação implícita. Existem dois casos comuns:
    • 1 - Cada instrução SQL individual faz parte de uma transação, aberta antes da instrução e fechada depois. Isto é conhecido como modo autocommit. Tudo se comporta, portanto, como se o cliente JPA estivesse a realizar transações para cada instrução SQL.
    • 2 - O SGBD não está no modo autocommit e inicia uma transação implícita na primeira instrução SQL que o cliente JPA emite fora de uma transação, deixando a cargo do cliente o seu encerramento. Todas as instruções SQL emitidas pelo cliente JPA fazem então parte da transação implícita. Esta transação pode terminar devido a vários eventos: o cliente encerra a ligação, inicia uma nova transação, etc.

Esta situação depende da configuração do SGBD. Por conseguinte, o código não é portátil. Mostraremos mais adiante um exemplo de código sem transações e veremos que nem todos os SGBDs se comportam da mesma forma com este código. Consideraremos, portanto, que trabalhar fora de transações constitui um erro de programação.

  • Linha 7: Note que o número da versão foi atualizado para 2.

2.1.13.7. Teste 6

O código para o Teste 6 é o seguinte:


// supprimer un objet n'appartenant pas au contexte de persistance
    public static void test6() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on supprime p1 qui n'appartient pas au nouveau contexte
        try {
            em.remove(p1);
            // fin transaction
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
            // on fait un rollback de la transaction
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // on affiche la nouvelle table
        dump();
    }
  • O Teste 6 tenta eliminar um objeto que não pertence ao contexto de persistência.
  • linha 4: é solicitado um novo contexto de persistência. O antigo é, portanto, encerrado, e os objetos que continha ficam desanexados. É o caso do objeto p1 do teste 5 anterior.
  • Linhas 6–7: Início da transação.
  • Linha 10: O objeto desanexado p1 é eliminado. Sabemos que isto irá causar uma exceção, por isso envolvemos a operação num bloco try/catch.
  • Linha 12: O commit não será executado.
  • Linhas 16–21: Uma transação deve terminar com um commit (todas as operações na transação são validadas) ou um rollback (todas as operações na transação são revertidas). Ocorreu uma exceção, por isso revertemos a transação. Não há nada para desfazer, uma vez que a única operação na transação falhou, mas o rollback encerra a transação. Esta é a primeira vez que usamos a operação [EntityTransaction].rollback. Devíamos ter feito isto desde os primeiros exemplos. Não o fizemos para manter o código simples. O leitor deve, no entanto, ter em mente que o caso de um rollback de transação deve ser sempre considerado no código.
  • Linha 24: Mostramos a tabela. Normalmente, não deveria ter sofrido alterações.

A saída da consola para o Teste 6 é a seguinte:

1
2
3
4
5
6
7
8
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • linha 6: Falha na eliminação de p1. A mensagem de exceção explica que foi feita uma tentativa de eliminar um objeto destacado, que não faz parte do contexto. Isso não é possível.
  • Linha 8: A pessoa p1 ainda está lá.

2.1.13.8. Teste 7

O código para o Teste 7 é o seguinte:


// modifier un objet n'appartenant pas au contexte de persistance
    public static void test7() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on incrémente le nbre d'enfants de p1 qui n'appartient pas au nouveau contexte
        p1.setNbenfants(p1.getNbenfants() + 1);
        // fin transaction
        tx.commit();
        // on affiche la nouvelle table - elle n'a pas du changer
        dump();
    }
  • O Teste 7 tenta modificar um objeto que não pertence ao contexto de persistência e observa o impacto que isso tem na base de dados. Seria de esperar que não houvesse nenhum. É isso que os resultados do teste mostram.
  • Linha 4: É solicitado um novo contexto de persistência. Temos, portanto, um novo contexto sem objetos persistentes.
  • Linhas 6–7: Início da transação.
  • Linha 9: O objeto destacado p1 é modificado. Esta é uma operação que não envolve o contexto de persistência em. Portanto, não devemos esperar uma exceção ou algo do género. É uma operação básica num POJO.
  • Linha 11: O commit sincroniza o contexto com a base de dados. Este contexto está vazio. Por conseguinte, a base de dados permanece inalterada.
  • Linha 24: A tabela é apresentada. Normalmente, não deveria ter sofrido alterações.

A saída da consola para o teste 7 é a seguinte:

1
2
3
4
5
6
7
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • linha 7: a pessoa p1 não sofreu alterações na base de dados. Para o próximo teste, no entanto, teremos em conta que, na memória, o número de filhos é agora 5.

2.1.13.9. Teste 8

O código para o teste 8 é o seguinte:


    // réattacher un objet au contexte de persistance
    public static void test8() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on réattache l'objet détaché p1 au nouveau contexte
        newp1 = em.merge(p1);
        // c'est newp1 qui fait désormais partie du contexte, pas p1
        // fin transaction
        tx.commit();
        // on affiche la nouvelle table - le nbre d'enfants de p1 a du changer
        dump();
}
  • O Teste 8 volta a associar um objeto desassociado ao contexto de persistência.
  • Linha 4: É solicitado um novo contexto de persistência. Temos, portanto, um novo contexto sem objetos persistentes.
  • Linhas 6-7: início da transação.
  • Linha 9: O objeto desanexado p1 é reanexado ao contexto de persistência. A operação de fusão pode envolver vários cenários:
    • Caso 1: Existe um objeto persistente ps1 no contexto de persistência com a mesma chave primária que o objeto desanexado p1. O conteúdo de p1 é copiado para ps1, e a fusão retorna uma referência a ps1.
    • Caso 2: Não existe nenhum objeto persistente ps1 no contexto de persistência com a mesma chave primária que o objeto desanexado p1. A base de dados é então consultada para determinar se o objeto procurado existe na base de dados. Se for o caso, este objeto é trazido para o contexto de persistência, torna-se o objeto persistente ps1 e voltamos ao Caso 1 anterior.
    • Caso 3: Não existe nenhum objeto com a mesma chave primária que o objeto desanexado p1, nem no contexto de persistência nem na base de dados. É então criado um novo objeto [Person] (new) e colocado no contexto de persistência. Regressamos então ao Caso 1.
    • No final: o objeto desanexado p1 permanece desanexado. A operação de fusão retorna uma referência (aqui newp1) ao objeto persistente ps1 resultante da fusão. A aplicação cliente deve agora trabalhar com o objeto persistente ps1 e não com o objeto desanexado p1.
    • Note a diferença entre os casos 1 e 3 no que diz respeito à instrução SQL utilizada para a fusão: nos casos 1 e 2, trata-se de uma instrução UPDATE, enquanto no caso 3, trata-se de uma instrução INSERT.
  • Linha 12: O commit sincroniza o contexto com a base de dados. Este contexto já não está vazio. Contém o objeto newp1. Este objeto será persistido na base de dados.
  • Linha 24: Apresentamos a tabela para a verificar.

A saída da consola para o Teste 8 é a seguinte:

main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test8
[personnes]
[1,3,Martin,Paul,31/01/2000,false,5]
  • O número de filhos de p1 era 4 no teste 6 (linha 4), depois mudou para 5 no teste 7, mas não foi guardado na base de dados (linha 7). Após a fusão, newp1 foi guardado na base de dados: linha 10, agora temos 5 filhos.
  • Linha 10: O número da versão do newp1 foi atualizado para 3.

2.1.13.10. Teste 9

O código para o Teste 9 é o seguinte:


// a select request causes synchronization
    // with the persistence context
    public static void test9() {
        // persistence context
        EntityManager em = getEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // increment the number of children of newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // people display - the number of children in newp1 must have changed
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // end transaction
        tx.commit();
    }
  • O Teste 9 demonstra o mecanismo de sincronização de contexto que ocorre automaticamente antes de uma instrução SELECT.
  • linha 5: o contexto de persistência não é alterado. newp1 encontra-se, portanto, dentro dele.
  • Linhas 7–8: Início da transação.
  • Linha 10: O número de filhos do objeto persistente newp1 é aumentado em 1 (5 -> 6).
  • Linhas 12–15: A tabela é apresentada utilizando uma instrução SELECT. O contexto será sincronizado com a base de dados antes da execução da instrução SELECT.
  • Linha 17: Fim da transação

Para visualizar a sincronização, ative a saída de log do Hibernate no modo DEBUG (log4j.properties):


# Root logger option
log4j.rootLogger=ERROR, stdout
 
# Hibernate logging options (INFO only shows startup messages)
log4j.logger.org.hibernate=DEBUG

A saída da consola para o teste 9 é a seguinte:

main : ----------- test9
14:27:27,250 DEBUG JDBCTransaction:54 - begin
14:27:27,250 DEBUG ConnectionManager:415 - opening JDBC connection
14:27:27,250 DEBUG JDBCTransaction:59 - current autocommit status: true
14:27:27,250 DEBUG JDBCTransaction:62 - disabling autocommit
14:27:27,250 DEBUG JDBCContext:210 - after transaction begin
[personnes]
14:27:27,250 DEBUG QueryPlanCache:76 - located HQL query plan in cache (select p from Personne p order by p.nom asc)
14:27:27,250 DEBUG AbstractFlushingEventListener:58 - flushing session
...
14:27:27,250 DEBUG AbstractEntityPersister:3116 - entites.Personne.nbenfants is dirty
14:27:27,250 DEBUG DefaultFlushEntityEventListener:229 - Updating entity: [entites.Personne#1]
14:27:27,250 DEBUG Versioning:27 - Incrementing: 3 to 4
...
14:27:27,250 DEBUG AbstractFlushingEventListener:85 - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects
...
14:27:27,250 DEBUG ConnectionManager:463 - registering flush begin
14:27:27,250 DEBUG AbstractEntityPersister:2274 - Updating entity: [entites.Personne#1]
14:27:27,265 DEBUG AbstractEntityPersister:2276 - Existing version: 3 -> New version: 4
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - update jpa01_personne set VERSION=?, NOM=?, PRENOM=?, DATENAISSANCE=?, MARIE=?, NBENFANTS=? where ID=? and VERSION=?
14:27:27,265 DEBUG AbstractBatcher:476 - preparing statement
14:27:27,265 DEBUG AbstractEntityPersister:1927 - Dehydrating entity: [entites.Personne#1]
14:27:27,265 DEBUG IntegerType:80 - binding '4' to parameter: 1
14:27:27,265 DEBUG StringType:80 - binding 'Martin' to parameter: 2
14:27:27,265 DEBUG StringType:80 - binding 'Paul' to parameter: 3
14:27:27,265 DEBUG DateType:80 - binding '31 janvier 2000' to parameter: 4
14:27:27,265 DEBUG BooleanType:80 - binding 'false' to parameter: 5
14:27:27,265 DEBUG IntegerType:80 - binding '6' to parameter: 6
14:27:27,265 DEBUG IntegerType:80 - binding '1' to parameter: 7
14:27:27,265 DEBUG IntegerType:80 - binding '3' to parameter: 8
14:27:27,265 DEBUG AbstractBatcher:366 - about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
14:27:27,265 DEBUG AbstractBatcher:525 - closing statement
14:27:27,265 DEBUG ConnectionManager:472 - registering flush end
14:27:27,265 DEBUG HQLQueryPlan:150 - find: select p from Personne p order by p.nom asc
14:27:27,265 DEBUG QueryParameters:277 - named parameters: {}
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - select personne0_.ID as ID0_, personne0_.VERSION as VERSION0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.DATENAISSANCE as DATENAIS5_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_ from jpa01_personne personne0_ order by personne0_.NOM asc
...
14:27:27,265 DEBUG Loader:1164 - result row: EntityKey[entites.Personne#1]
...
14:27:27,265 DEBUG Loader:839 - total objects hydrated: 0
14:27:27,265 DEBUG StatefulPersistenceContext:748 - initializing non-lazy collections
[1,4,Martin,Paul,31/01/2000,false,6]
14:27:27,265 DEBUG JDBCTransaction:103 - commit
14:27:27,265 DEBUG SessionImpl:337 - automatically flushing session
...
14:27:27,265 DEBUG AbstractFlushingEventListener:91 - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
...
14:27:27,296 DEBUG JDBCTransaction:116 - committed JDBC Connection
...
  • Linha 1: Início do Teste 9
  • linhas 2–6: a transação JDBC inicia. O modo de autocommit do SGBD está desativado (linha 5)
  • linha 7: exibição acionada pela linha 12 do código Java. As linhas seguintes do código Java irão acionar um SELECT e, assim, sincronizar o contexto de persistência com a base de dados.
  • linha 8: a consulta JPQL que pretendemos executar já foi executada. O Hibernate encontra-a no seu cache de «consultas preparadas».
  • Linha 9: o Hibernate anuncia que irá atualizar o contexto de persistência
  • Linhas 11–12: o Hibernate (Hb) deteta que a entidade Person#1 (com chave primária 1) foi modificada (dirty).
  • Linhas 12–13: O Hb anuncia que está a atualizar este elemento e incrementa o seu número de versão de 3 para 4.
  • Linha 15: A sincronização do contexto resultará em 0 inserções, 1 atualização e 0 eliminações
  • Linhas 17-34: Sincronização do contexto (flush). Nota: o incremento da versão (linha 19), a instrução SQL de atualização preparada (linha 21) e os valores dos parâmetros para a instrução de atualização (linhas 24-31).
  • Linha 35: A instrução SELECT começa
  • linha 38: a instrução SQL a ser executada
  • linha 40: a instrução SELECT retorna apenas uma linha
  • linha 42: o Hb descobre que já possui, no seu contexto de persistência, a entidade Person#1 que o SELECT devolveu da base de dados. Por isso, não copia a linha obtida da base de dados para o contexto, uma operação a que chama «hidratação».
  • linha 43: verifica se os objetos devolvidos pela instrução SELECT têm dependências (normalmente chaves estrangeiras) que também precisariam de ser carregadas (coleções não preguiçosas). Aqui, não há nenhuma.
  • Linha 44: Exibição acionada pelo código Java
  • Linha 45: Fim da transação JDBC solicitada pelo código Java
  • Linha 46: Inicia-se a sincronização automática do contexto, que ocorre durante os commits.
  • Linha 48: O Hb deteta que o contexto não se alterou desde a última sincronização.
  • Linha 50: Fim do commit.

Mais uma vez, os registos do Hibernate no modo DEBUG revelam-se muito úteis para compreender exatamente o que o Hibernate está a fazer.

2.1.13.11. Teste 10

O código para o teste 10 é o seguinte:


// contrôle de version (optimistic locking)
    public static void test10() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrémenter la version de newp1 directement dans la base (native query)
        em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
        // fin transaction
        tx.commit();
        // début nouvelle transaction
        tx = em.getTransaction();
        tx.begin();
        // on incrémente le nbre d'enfants de newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // fin transaction - elle doit échouer car newp1 n'a plus la bonne version
        try {
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
            // on fait un rollback de la transaction
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // on ferme le contexte qui n'est plus à jour
        em.close();
        // dump de la table - la version de p1 a du changer
        dump();
    }
  • O Teste 10 demonstra o mecanismo introduzido pelo campo version da @Entity Person, que é anotado com a anotação JPA @Version. Explicámos que esta anotação faz com que o valor da coluna associada à anotação @Version seja incrementado na base de dados a cada atualização feita na linha a que pertence. Este mecanismo, também conhecido como bloqueio otimista, exige que um cliente que pretenda modificar um objeto O na base de dados tenha a versão mais recente desse objeto. Se não a tiver, significa que o objeto foi modificado desde que o cliente o obteve, e o cliente deve ser notificado.
  • Linha 4: Não alteramos o contexto de persistência. newp1 encontra-se, portanto, dentro dele.
  • Linhas 6–7: Início de uma transação.
  • Linha 9: A versão do objeto newp1 é incrementada em 1 (4 -> 5) diretamente na base de dados. As consultas do tipo nativeQuery contornam o contexto de persistência e escrevem diretamente na base de dados. O resultado é que o objeto persistente newp1 e a sua representação na base de dados já não têm a mesma versão.
  • Linha 10: fim da primeira transação
  • Linhas 13–14: Início de uma segunda transação
  • Linha 16: o número de filhos do objeto persistente newp1 é aumentado em 1 (6 -> 7).
  • Linha 19: fim da transação. A sincronização ocorre, portanto. Isto irá desencadear uma atualização do número de filhos de newp1 na base de dados. Isto irá falhar porque o objeto persistente newp1 tem a versão 4, enquanto que na base de dados o objeto a ser atualizado tem a versão 5. Será lançada uma exceção, o que justifica o bloco try/catch no código.
  • Linha 21: A exceção e a sua causa são apresentadas.
  • Linha 25: Reverter a transação
  • Linha 33: Exibir a tabela: devemos ver que a versão de newp1 é 5 na base de dados.

A saída da consola para o teste 10 é a seguinte:

1
2
3
4
5
6
7
main : ----------- test9
[personnes]
[1,4,Martin,Paul,31/01/2000,false,6]
main : ----------- test10
Erreur lors de la mise à jour de newp1 [javax.persistence.RollbackException,Error while commiting the transaction,org.hibernate.StaleObjectStateException,Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [entites.Personne#1]]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • Linha 5: O commit lança, de facto, uma exceção. É do tipo [javax.persistence.RollbackException]. A mensagem associada é vaga. Se analisarmos a causa desta exceção (Exception.getCause), verificamos que temos uma exceção do Hibernate devido ao facto de estarmos a tentar modificar uma linha na base de dados sem ter a versão correta.
  • Linha 7: Vemos que a versão de newp1 na base de dados foi, de facto, definida como 5 pelo nativeQuery.

2.1.13.12. Teste 11

O código para o teste 11 é o seguinte:


// transaction rollback
    public static void test11() throws ParseException {
        // persistence context
        EntityManager em = getEntityManager();
        // start of transaction
        EntityTransaction tx = null;
        try {
            tx = em.getTransaction();
            tx.begin();
            // reattach p1 to the context by fetching it from the base
            p1 = em.find(Personne.class, p1.getId());
            // increment the number of children in p1
            p1.setNbenfants(p1.getNbenfants() + 1);
            // display people - the number of children in p1 must have changed
            System.out.println("[personnes]");
            for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
                System.out.println(p);
            }
            // creation of 2 persons with identical names, which is forbidden by the DDL
            Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            // persistence of people
            em.persist(p3);
            em.persist(p4);
            // end transaction
            tx.commit();
        } catch (RuntimeException e1) {
            // we had a problem
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // we abandon the current context
            em.clear();
        }
        // dump - table must not have changed due to rollback
        dump();
    }
  • O Teste 11 centra-se no mecanismo de reversão de transações. Uma transação funciona numa base de «tudo ou nada»: as operações SQL que contém são todas executadas com sucesso (confirmação) ou todas revertidas se alguma delas falhar (reversão).
  • linha 4: continuamos com o mesmo contexto de persistência. O leitor deve lembrar-se de que o contexto foi fechado após a falha no teste anterior. Neste caso, [getEntityManager] devolve um contexto totalmente novo e, portanto, vazio.
  • Linhas 7–27: Um único bloco try/catch para lidar com quaisquer problemas que possam surgir
  • Linhas 8–9: Início de uma transação que irá conter várias operações SQL
  • Linha 11: p1 é recuperado da base de dados e colocado no contexto
  • Linha 13: O número de filhos de p1 é aumentado (6 → 7)
  • Linhas 15–18: Exibimos o conteúdo da base de dados, o que forçará uma sincronização do contexto. Na base de dados, o número de filhos de p1 mudará para 7, o que a saída da consola deverá confirmar.
  • Linhas 20–21: Criação de duas pessoas, p3 e p4, com o mesmo nome. No entanto, o campo name da @Entity Person tem o atributo unique=true, o que resulta numa restrição de unicidade na coluna NAME da tabela [jpa01_personne].
  • Linhas 23–24: As pessoas p3 e p4 são adicionadas ao contexto de persistência.
  • Linha 26: A transação é confirmada. Segue-se uma segunda sincronização do contexto, tendo a primeira ocorrido durante a instrução SELECT. O JPA emitirá duas instruções SQL INSERT para as pessoas p3 e p4. A p3 será inserida. Para a p4, o DBMS lançará uma exceção porque a p4 tem o mesmo nome que a p3. A p4 não é, portanto, inserida, e o controlador JDBC lança uma exceção para o cliente.
  • Linha 27: Tratamos a exceção
  • Linhas 29–31: Exibimos a exceção e as duas causas que a precederam na cadeia de exceções que nos levou a este ponto.
  • Linha 34: Revertemos a transação atualmente ativa. Esta transação começou na linha 9 do código Java. Desde então, foi realizada uma operação de atualização para alterar o número de filhos de p1, seguida de uma operação de inserção para a pessoa p3. Tudo isto será desfeito pela reversão.
  • Linha 39: o contexto de persistência é limpo
  • Linha 42: A tabela [jpa01_personne] é apresentada. Temos de verificar se p1 ainda tem 6 filhos e se nem p3 nem p4 constam da tabela.

A saída da consola para o teste 11 é a seguinte:


main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • linha 3: o número de filhos de p1 mudou de 6 para 7 na base de dados; a versão de p1 foi atualizada para 6.
  • Linha 4: A exceção detetada durante a confirmação da transação. Se ler com atenção, poderá ver que a causa é uma chave duplicada X (o nome). É a inserção de p4 que causa este erro, uma vez que p3, que já foi inserido, também tem o nome X.
  • Linha 7: A tabela após o rollback. p1 voltou à versão 5 e tem novamente 6 filhos; p3 e p4 não foram inseridos.

2.1.13.13. Teste 12

O código para o teste 12 é o seguinte:


    // we do the same thing again but without the transactions
    // we obtain the same result as before with SGBD : FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
    // with SQLSERVER we have an empty table. The connection is left in a state that prevents reexecution
    // of the program. The server must then be restarted.
    // idem with SGBD Derby
    // HSQL inserts 1st person - there is no rollback
 
    public static void test12() throws ParseException {
        // reconnect p1
        p1 = em.find(Personne.class, p1.getId());
        // increment the number of children in p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // display people - the number of children in p1 must have changed
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // creation of 2 persons with identical names, which is forbidden by the DDL
        Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        // persistence of people
        em.persist(p3);
        em.persist(p4);
        // dump, which will sync the em context with the BD
        try {
            dump();
        } catch (RuntimeException e3) {
            System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
                    .getCause().getMessage());
        }
        // we close the current context
        em.close();
        // dump
        dump();
}
  • O Teste 12 repete o mesmo processo do Teste 11, mas fora de uma transação. Queremos ver o que acontece neste caso.
  • Linhas 1–6: mostram os resultados do teste com vários SGBDs:
  • com vários SGBDs (Firebird, Oracle, MySQL5, Postgres), obtemos o mesmo resultado que no Teste 11. Isto sugere que estes SGBDs iniciaram uma transação por conta própria, abrangendo todas as instruções SQL recebidas até àquela que causou o erro, e que eles próprios iniciaram um rollback.
  • Com outros SGBDs (SQL Server, Apache Derby), a aplicação e/ou o SGBD entra em falha.
  • Com o SGBD HSQLDB, parece que a transação aberta pelo SGBD está no modo de autocommit: a modificação do número de filhos de p1 e a inserção de p3 tornam-se permanentes. Apenas a inserção de p4 falha.

Temos, portanto, um resultado que depende do SGBD, o que torna a aplicação não portátil. Note-se que as operações no contexto de persistência devem ser sempre realizadas dentro de uma transação.

2.1.14. Mudança do SGBD

Vamos rever a arquitetura de teste do nosso projeto atual:

A aplicação cliente [3] vê apenas a interface JPA [5]. Não vê nem a sua implementação real nem o SGBD de destino. Temos, portanto, de ser capazes de alterar estes dois elementos da cadeia sem fazer alterações no cliente [3]. É isso que vamos agora tentar demonstrar, começando por alterar o SGBD. Até agora, temos utilizado o MySQL5. Apresentamos outros seis descritos nos apêndices (secção 5), na esperança de que entre eles se encontre o SGBD preferido do leitor.

De qualquer forma, a modificação a ser feita no projeto Eclipse é simples (ver abaixo): substitua o ficheiro de configuração persistence.xml [1] da camada JPA por um dos que se encontram na pasta conf [2] do projeto. Os controladores JDBC para estes SGBDs já estão presentes nas bibliotecas [jpa-divers] [3] e [4].

2.1.14.1. Oracle 10g Express

O Oracle 10g Express é apresentado nos apêndices, na secção 5.7. O ficheiro persistence.xml do Oracle é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Persistent classes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- logs SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  automatic schematic creation -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  properties DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Esta configuração é idêntica à utilizada para o SGBD MySQL5, com as seguintes pequenas diferenças:

  • linhas 15–18, que configuram a ligação JDBC à base de dados
  • linha 22: que define o dialeto SQL a utilizar

Nos exemplos a seguir, especificaremos apenas as linhas que se alteram. Para uma explicação da configuração, consulte o apêndice dedicado ao SGBD em uso. É fornecido ali um exemplo de utilização da ligação JDBC em cada caso, no contexto do plugin [SQL Explorer]. Com as informações do apêndice, o leitor pode repetir o processo de verificação do resultado da aplicação [InitDB] realizado na secção 2.1.10.2.

Procedemos conforme indicado na secção acima referida:

  • inicie o SGBD Oracle
  • colocar conf/oracle/persistence.xml em META-INF/persistence.xml
  • executar a aplicação [InitDB]

Os seguintes resultados aparecem na consola:

A partir de agora, não voltaremos a mostrar esta captura de ecrã, uma vez que permanece inalterada. Mais interessante é a vista do SQL Explorer da ligação JDBC ao SGBD. Seguiremos o procedimento explicado na secção 2.1.8.

  • em [1]: a ligação ao Oracle
  • em [2]: a árvore de ligações após a execução de [InitDB]
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: o seu conteúdo.

Depois de fazer isto, convidamos o leitor a executar a aplicação [Main] e, em seguida, a encerrar o SGBD.

2.1.14.2. PostgreSQL 8.2

O PostgreSQL 8.2 é apresentado nos Apêndices, na secção 5.6. O seu ficheiro persistence.xml é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
            <property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
    </persistence-unit>
</persistence>

Para executar [InitDB]:

  • Inicie o SGBD PostgreSQL
  • Coloque o ficheiro conf/postgres/persistence.xml em META-INF/persistence.xml
  • executar a aplicação [InitDB]

A vista do SQL Explorer da ligação JDBC ao SGBD é a seguinte:

  • em [1]: a ligação ao PostgreSQL
  • em [2]: a árvore de ligações após a execução de [InitDB]
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: o seu conteúdo.

Feito isto, convida-se o leitor a executar a aplicação [Main] e, em seguida, a encerrar o SGBD

2.1.14.3. SQL Server Express 2005

O SQL Server Express 2005 é apresentado nos Apêndices, na secção 5.8, página 270. O seu ficheiro persistence.xml é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
            <property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • Inicie o SGBD SQL Server
  • coloque conf/sqlserver/persistence.xml em META-INF/persistence.xml
  • execute a aplicação [InitDB]

A vista do SQL Explorer da ligação JDBC ao SGBD é a seguinte:

  • em [1]: a ligação ao SQL Server
  • em [2]: a árvore de ligações após a execução de [InitDB]
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: o seu conteúdo.

Feito isto, convida-se o leitor a executar a aplicação [Main] e, em seguida, a encerrar o SGBD

2.1.14.4. Firebird 2.0

O Firebird 2.0 é apresentado nos Apêndices, na secção 5.4. O seu ficheiro persistence.xml é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
            <property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
            <property name="hibernate.connection.username" value="sysdba" />
            <property name="hibernate.connection.password" value="masterkey" />
...
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
    </persistence-unit>
</persistence>

Para executar [InitDB]:

  • Inicie o SGBD Firebird
  • coloque conf/firebird/persistence.xml em META-INF/persistence.xml
  • execute a aplicação [InitDB]

A vista do SQL Explorer da ligação JDBC ao SGBD é a seguinte:

  • em [1]: a ligação ao Firebird
  • em [2]: a árvore de ligações após a execução de [InitDB]
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: o seu conteúdo.

Feito isto, convida-se o leitor a executar a aplicação [Main] e, em seguida, a encerrar o SGBD.

2.1.14.5. Apache Derby

O Apache Derby é apresentado nos Apêndices, na Secção 5.10. O seu ficheiro persistence.xml é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
            <property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialect -->
...
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • inicie o SGBD Apache Derby
  • coloque conf/derby/persistence.xml em META-INF/persistence.xml
  • execute a aplicação [InitDB]

A vista do SQL Explorer da ligação JDBC ao SGBD é a seguinte:

  • em [1]: a ligação ao Apache Derby
  • em [2]: a árvore de ligações após a execução de [InitDB]. Repare na tabela [HIBERNATE_UNIQUE_KEY] criada pelo JPA/Hibernate para gerar automaticamente valores sucessivos para o ID da chave primária. Já referimos que este mecanismo é frequentemente proprietário. Isso é claramente evidente aqui. Graças ao JPA, o programador não precisa de se aprofundar nestes detalhes do SGBD.
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: o seu conteúdo.

Uma vez feito isto, convidamos o leitor a executar a aplicação [Main] e, em seguida, a encerrar o SGBD.

2.1.14.6. HSQLDB

O HSQLDB é apresentado nos Apêndices, na Secção 5.9. O seu ficheiro persistence.xml é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- connection JDBC -->
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
            <property name="hibernate.connection.username" value="sa" />
            <!-- 
                <property name="hibernate.connection.password" value="" />
            -->
...
            <!-- Dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
        </properties>
    </persistence-unit>
</persistence>

Para executar [InitDB]:

  • inicie o SGBD HSQL
  • Coloque o ficheiro conf/hsql/persistence.xml em META-INF/persistence.xml
  • execute a aplicação [InitDB]

A vista do SQL Explorer da ligação JDBC ao SGBD é a seguinte:

  • em [1]: a ligação ao HSQL
  • em [2]: a árvore de ligações após a execução de [InitDB].
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: o seu conteúdo.

Feito isto, convida-se o leitor a executar a aplicação [Main] e, em seguida, a parar o SGBD.

2.1.15. Alterar a implementação do JPA

Vamos rever a arquitetura de teste do nosso projeto atual:

O estudo anterior demonstrou que conseguimos alterar o SGBD [7] sem alterar nada no código do cliente [3]. Iremos agora alterar a implementação do JPA [6] e demonstrar, mais uma vez, que isto pode ser feito de forma transparente para o código do cliente [3]. Iremos utilizar uma implementação do TopLink [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:

2.1.15.1. O Projeto Eclipse

Em conjunto com a alteração na implementação da JPA, estamos a criar um novo projeto Eclipse para não contaminar o projeto existente. De facto, o novo projeto utiliza bibliotecas de persistência que podem entrar em conflito com as do Hibernate:

  • em [1]: a pasta [<examples>/toplink/direct/people-entities] contém o projeto Eclipse. Importe-o.
  • em [2]: o projeto importado [toplink-personnes-entites]. É idêntico (foi copiado) ao projeto [hibernate-personne-entites], com exceção de dois detalhes:
    • o ficheiro [META-INF/persistence.xml] [3] configura agora uma camada JPA/Toplink
    • a biblioteca [jpa-hibernate] foi substituída pela biblioteca [jpa-toplink] [4] e [5] (ver parágrafo 1.5).
  • em [6]: a pasta [conf] contém uma versão do ficheiro [persistence.xml] para cada SGBD.
  • em [7]: a pasta [ddl], que conterá os scripts SQL para gerar o esquema da base de dados.

Sabemos que a camada JPA é configurada pelo ficheiro [META-INF/persistence.xml]. Este ficheiro configura agora uma implementação JPA / Toplink. O seu conteúdo para uma camada JPA que faz interface com o SGBD MySQL5 é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- persistent classes -->
        <class>entites.Personne</class>
        <!-- persistence unit properties -->
        <properties>
            <!-- connection JDBC -->
            <property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="MySQL4" />
            <!--  application server -->
            <property name="toplink.target-server" value="None" />
            <!--  generation diagram -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- logs -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>
  • Linha 3: inalterada
  • linha 5: o fornecedor é agora o Toplink. A classe aqui indicada pode ser encontrada na biblioteca [jpa-toplink] ([1] abaixo):
  • linha 7: a tag <class> é utilizada para listar todas as classes @Entity no projeto; aqui, apenas a classe Person. O Hibernate tinha uma opção de configuração que nos permitia evitar listar estas classes. Ele analisava o classpath do projeto para encontrar as classes @Entity.
  • linha 9: a tag <properties> introduz propriedades específicas da implementação JPA que está a ser utilizada, neste caso o Toplink.
  • Linhas 11–14: Configuração da ligação JDBC ao SGBD MySQL5
  • Linhas 15–18: Configuração do pool de ligações JDBC gerido nativamente pelo Toplink:
  • Linhas 15, 16: número máximo e mínimo de ligações no conjunto de ligações de leitura. Padrão (2,2)
  • Linhas 17, 18: Número máximo e mínimo de ligações no conjunto de ligações de escrita. Padrão (10,2)
  • linha 20: o SGBD de destino. A lista de SGBDs suportados está disponível no pacote [oracle.toplink.essentials.platform.database] (ver [2] acima). O SGBD MySQL5 não está incluído na lista [2], pelo que optámos pelo MySQL4. O TopLink suporta um número ligeiramente inferior de SGBDs em comparação com o Hibernate. Assim, dos sete SGBDs utilizados nos nossos exemplos, o Firebird não é suportado. O Oracle também não se encontra na lista. Na verdade, está noutro pacote ([3] acima). Se, nestes dois pacotes, o SGBD de destino for designado pela classe <Sgbd>Platform.class, a tag será escrita como:

            <property name="toplink.target-database" value="<Sgbd>" />
  • Linha 22: Define o servidor de aplicações se a aplicação for executada num servidor desse tipo. Valores possíveis atuais (None, OC4J_10_1_3, SunAS9). Padrão (None).
  • Linhas 24–28: Quando a camada JPA é inicializada, recebe a instrução para limpar a base de dados definida pela ligação JDBC nas linhas 11–14. Isto garante que começamos com uma base de dados vazia.
    • Linha 24: O TopLink recebe a instrução para eliminar e, em seguida, criar as tabelas no esquema da base de dados
    • Linha 25: Instruímos o TopLink a gerar os scripts SQL para as operações de eliminação e criação. application-location especifica o diretório onde esses scripts serão gerados. Padrão: (diretório atual).
    • Linha 26: Nome do script SQL para as operações de criação. Padrão: createDDL.jdbc.
    • Linha 27: Nome do script SQL para as operações de eliminação. Padrão: dropDDL.jdbc.
    • Linha 28: modo de geração do esquema (Padrão: ambos):
      • both: scripts e base de dados
      • base de dados: apenas base de dados
      • script-sql: apenas scripts
  • Linha 30: O registo do TopLink está desativado (OFF). Os níveis de registo disponíveis são: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. Padrão: INFO.

Consulte o URL [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html] para obter uma definição abrangente das tags <property> que podem ser utilizadas com o TopLink.

2.1.15.3. Teste [InitDB]

Não há mais nada a fazer. Estamos prontos para executar o primeiro teste [InitDB]:

  • Inicie o SGBD, neste caso o MySQL5
  • execute [InitDB]
  • em [1]: o ecrã da consola. Vemos os resultados já obtidos com JPA / Hibernate.
  • em [3]: abra a perspetiva [SQL Explorer] e, em seguida, abra a ligação [mysql5-jpa]
  • em [4]: a árvore da base de dados JPA. Vemos que a execução de [InitDB] criou duas tabelas: [jpa01_personne], o que era esperado, e a tabela [sequence], o que era menos esperado.
  • em [5]: a estrutura da tabela [jpa01_personne] e em [6] o seu conteúdo
  • Em [7]: a estrutura da tabela [sequence] e, em [8], o seu conteúdo.

O ficheiro de configuração [persistence.xml] solicitou a geração de scripts DDL:


            <!--  génération schéma -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />

Vamos ver o que foi gerado na pasta [ddl/mysql5]:

 

create.sql


CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • Linha 1: O DDL para a tabela [jpa01_personne]. Note-se que o Toplink não utilizou o atributo autoincrement para a chave primária ID. Consequentemente, o ID não é incrementado automaticamente quando são inseridas novas linhas.
  • Linha 2: O DDL para a tabela [sequence]. O seu nome sugere que o Toplink utiliza esta tabela para gerar valores para a chave primária ID.
  • Linha 3: Inserção de uma única linha na tabela [SEQUENCE]

drop.sql


DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
  • Linha 1: Eliminação da tabela [jpa01_personne]
  • Linha 2: Elimina uma linha específica da tabela [SEQUENCE]. A tabela em si não é eliminada, nem quaisquer outras linhas que possa conter.

Para saber mais sobre a função da tabela [SEQUENCE], ative os registos do TopLink no nível FINE no ficheiro [persistence.xml], um nível que rastreia as instruções SQL emitidas pelo TopLink:


            <!-- logs -->
<property name="toplink.logging.level" value="FINE" />

Execute o InitDB novamente. Segue-se uma visualização parcial da saída da consola:


...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--Connected: jdbc:mysql://localhost:3306/jpa
    User: jpa@localhost
    Database: MySQL  Version: 5.0.37-community-nt
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Thread(Thread[main,5,main])--Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007)): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--delete from jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
    bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--disconnect
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Thread(Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa logout successful
...
terminé ...
  • Linhas 2-5: uma ligação ao SGBD com os seus parâmetros. Na verdade, os registos mostram que o Toplink cria, na realidade, 3 ligações ao SGBD. Temos de verificar se este número está relacionado com um dos valores de configuração utilizados para o conjunto de ligações JDBC:

            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
  • Linha 7: Eliminação da tabela [jpa01_personne]. Isto é normal, uma vez que o ficheiro [persistence.xml] solicita a limpeza da base de dados JPA.
  • linha 8: criação da tabela [jpa01_personne]. Note-se que a chave primária ID não possui o atributo autoincrement.
  • Linha 9: criação da tabela [SEQUENCE], que já existe, tendo sido criada durante a execução anterior.
  • Linhas 10–13: O TopLink reporta um erro ao criar a tabela [SEQUENCE].
  • Linhas 15–18: O TopLink limpa a tabela [SEQUENCE]. Após esta limpeza, a tabela [SEQUENCE] contém uma linha (SEQ_NAME, SEQ_COUNT) com os valores ('SEQ_GEN', 1).
  • Linha 18: A tabela [jpa01_personne] é esvaziada.
  • Linhas 19–20: O TopLink atualiza a única linha em que SEQ_NAME = 'SEQ_GEN' na tabela [SEQUENCE], alterando o valor de ('SEQ_GEN', 1) para ('SEQ_GEN', 51).
  • Linha 21: O TopLink recupera o valor 51 da linha ('SEQ_GEN', 51) na tabela [SEQUENCE].
  • Linhas 24–27: O Toplink insere as duas pessoas «Martin» e «Durant» na tabela [jpa01_personne]. Há aqui um mistério: às chaves primárias destas duas linhas são atribuídos os valores 2 e 3, sem qualquer explicação sobre como esses valores foram obtidos. Não é claro se o valor SEQ_COUNT (51) obtido na linha 21 serviu para algum propósito. Note-se que o valor da versão das linhas é 1, enquanto o Hibernate começou em 0.
  • Linha 28: O TopLink executa o SELECT para recuperar todas as linhas da tabela [jpa01_personne]
  • Linhas 29–30: Linhas exibidas pelo cliente Java
  • Linhas 31-32: O TopLink encerra uma ligação. Irá repetir a operação para cada uma das ligações inicialmente abertas.

Em última análise, não sabemos exatamente para que serve a tabela [SEQUENCE], mas parece que ela desempenha um papel na geração dos valores de ID da chave primária. Ao definir o nível de registo para o nível mais detalhado, FINEST, ficamos a saber um pouco mais sobre o papel da tabela [SEQUENCE].


            <!-- logs -->
            <property name="toplink.logging.level" value="FINEST" />

Abaixo, incluímos apenas os registos relativos à inserção das duas pessoas na tabela. É aqui que vemos o mecanismo de geração dos valores da chave primária:

[TopLink Finest]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Thread(Thread[main,5,main])--Execute query ValueReadQuery()
[TopLink Fine]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[TopLink Finest]: 2007.05.28 03:05:04.062--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--local sequencing preallocation for SEQ_GEN: objects: 50 , first: 2, last: 51
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--assign sequence to the object (2 -> [null,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Execute query DoesExistQuery()
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--PERSIST operation called on: [null,0,Durant,Sylvie,05/07/2001,false,0].
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--assign sequence to the object (3 -> [null,0,Durant,Sylvie,05/07/2001,false,0])
[personnes]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Execute query InsertObjectQuery([3,0,Durant,Sylvie,05/07/2001,false,0])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Assign return row DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Execute query InsertObjectQuery([2,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Assign return row DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
  • linha 4: vemos que o número 51 recuperado da tabela [SEQUENCE] na linha 2 é utilizado para delimitar um intervalo de valores para a chave primária: [2,51]
  • linha 5: à primeira pessoa é atribuído o valor 2 como chave primária
  • linha 8: à segunda pessoa é atribuído o valor 3 como chave primária
  • linha 12: mostra a gestão de versões para a primeira pessoa
  • linha 17: o mesmo para a segunda pessoa

O nível de registo [FINEST] também mostra os limites das transações emitidas pelo Toplink. A análise destes registos revela o que o Toplink faz e é uma excelente forma de compreender a ponte objeto-relacional.

Principais conclusões do exposto acima:

  • Diferentes implementações do JPA geram esquemas de base de dados diferentes. Neste exemplo, o Hibernate e o Toplink não geraram os mesmos esquemas.
  • Os níveis de registo FINE, FINER e FINEST do Toplink devem ser utilizados sempre que se pretenda esclarecer exatamente o que o Toplink está a fazer.

2.1.15.4. Teste [Principal]

Executamos agora o teste [Main]:

  • em [1]: todos os testes foram aprovados, exceto o teste 11 [2]
  • em [3]: linha 376, a linha de código onde ocorreu a exceção

O código que lança a exceção é o seguinte:


} catch (RuntimeException e1) {
            // on a eu un pb
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
            ...
  • linha [3]: a linha da exceção. Temos uma NullPointerException, o que sugere que um dos métodos getCause nas linhas 4 e 5 devolveu um ponteiro nulo. Uma expressão como [e1.getCause().getCause()] pressupõe que a cadeia de exceções tem 3 elementos [e1.getCause().getCause(), e1.getCause(), e1]. Se tiver apenas dois, a primeira expressão causará uma exceção.

Modificamos o código anterior para que ele exiba apenas as duas últimas exceções na cadeia de exceções:


        } catch (RuntimeException e1) {
            // on a eu un pb
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage());
            try {
...

Quando executado, obtemos o seguinte resultado:


...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- test11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]

Desta vez, o Teste 11 é aprovado. Os registos de exceção (linhas 6–10) foram acionados pelo código Java (linha 3 do código acima). Recorde-se que o Teste 11 encadeou, numa única transação, várias operações SQL, uma das quais falhou e esperava-se que causasse o rollback da transação. Os estados da tabela [jpa01_personne] antes (linha 3) e depois do teste (linha 12) são idênticos, mostrando que o rollback ocorreu.

É importante notar aqui que as implementações JPA/Hibernate e JPA/Toplink não são 100% intercambiáveis. Neste exemplo, temos de modificar o código do cliente JPA para evitar uma NullPointerException. Encontraremos esta questão novamente mais tarde, desta vez no contexto de uma exceção.

Vamos revisitar a arquitetura de teste do nosso projeto atual:

Anteriormente, o SGBD utilizado em [7] era o MySQL 5. Vamos demonstrar como mudar de SGBD utilizando o Oracle. Em qualquer caso, a modificação necessária no projeto Eclipse é simples (ver abaixo): substitua o ficheiro de configuração persistence.xml [1] da camada JPA por um dos ficheiros existentes na pasta conf do projeto ([2] e [3]).

2.1.16.1. Oracle 10g Express

O Oracle 10g Express é apresentado nos Apêndices, na secção 5.7. O ficheiro persistence.xml do Oracle para o Toplink é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- persistent classes -->
        <class>entites.Personne</class>
        <!-- persistence unit properties -->
        <properties>
            <!-- connection JDBC -->
            <property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
            <property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="Oracle" />
            <!--  application server -->
            <property name="toplink.target-server" value="None" />
            <!--  generation diagram -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/oracle" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- logs -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>

Esta configuração é idêntica à utilizada para o SGBD MySQL5, com as seguintes pequenas diferenças:

  • linhas 11–14, que configuram a ligação JDBC à base de dados
  • linha 20: que especifica o SGBD de destino
  • linha 25: que especifica o diretório para a geração de scripts SQL DDL

Para executar o teste [InitDB]:

  • inicie o SGBD Oracle
  • Coloque conf/oracle/persistence.xml em META-INF/persistence.xml
  • Execute a aplicação [InitDB]

Os seguintes resultados são apresentados na consola e na vista [SQL Explorer]:

  • [1]: o ecrã da consola
  • [2]: a ligação [oracle-jpa] no SQL Explorer
  • [3]: a base de dados JPA
  • [4]: O InitDB criou duas tabelas: JPA01_PERSONNE e SEQUENCE, tal como no MySQL5. Por vezes, em [4], aparecem tabelas [BIN*]. Estas correspondem a tabelas eliminadas. Para observar este fenómeno, basta executar novamente o [InitDB]. A fase de inicialização da camada JPA inclui uma limpeza da base de dados JPA, durante a qual a tabela [JPA01_PERSONNE] é eliminada:

Em [A], surge uma tabela [BIN]. O Oracle não elimina permanentemente uma tabela que tenha sido removida, mas coloca-a numa [Lixeira]. Esta Lixeira é visível [B] utilizando a ferramenta SQL Developer descrita na secção 5.7.4. Em [B], podemos eliminar a tabela [JPA01_PERSONNE] da Reciclagem. Isto esvazia a Reciclagem [C]. Se atualizarmos as tabelas no SQL Explorer (clique com o botão direito do rato / Atualizar), verificamos que a tabela BIN já não se encontra lá [D].

  • [5, 6]: a estrutura e o conteúdo da tabela [JPA01_PERSONNE]
  • [7, 8]: a estrutura e o conteúdo da tabela [SEQUENCE]

Pronto! O leitor é agora convidado a executar a aplicação [Main] no Oracle.

2.1.16.2. Outros SGBDs

Não abordaremos outros SGBDs em detalhe. Basta seguir o mesmo procedimento utilizado para o Oracle. Tenha em atenção os seguintes pontos:

  • Independentemente do SGBD, o Toplink utiliza sempre a mesma técnica para gerar os valores de ID da chave primária para a tabela [JPA01_PERSONNE]: utiliza a tabela [SEQUENCE] descrita acima.
  • O TopLink não suporta o SGBD Firebird. Existe uma configuração genérica de base de dados para esses casos:
                <property name="toplink.target-database" value="Auto" />

Com esta base de dados genérica denominada [Auto], os testes com o Firebird falham devido a erros de sintaxe SQL. O TopLink utiliza o tipo SQL Number(10) para o ID da chave primária, que o Firebird não reconhece. Por conseguinte, deve escolher um SGBD com os mesmos tipos SQL que o Firebird (para este exemplo). É o caso do Apache Derby:


            <!-- connexion JDBC -->
            <property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
            <!-- SGBD -->
            <!-- 
            TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
            -->
            <property name="toplink.target-database" value="Derby" />
...
  • O TopLink não consegue gerar o esquema de base de dados original para o SGBD HSQLDB. Ou seja, a diretiva:

            <!--  génération schéma -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />

falha no HSQLDB. A causa é um erro de sintaxe ao criar a tabela [jpa01_personne]:


[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Connection(29775659)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Connection(29775659)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Thread(Thread[main,5,main])--Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007)): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]

Linha 4: a sintaxe LAST_NAME VARCHAR(30) UNIQUE NOT NULL não é aceite pelo HSQL. O Hibernate utilizou a sintaxe: LAST_NAME VARCHAR(30) NOT NULL, UNIQUE(LAST_NAME).

Em geral, o Hibernate revelou-se mais eficaz do que o Toplink no reconhecimento dos SGBDs utilizados nos testes descritos neste documento.

2.1.17. Conclusão

O estudo da @Entity [Person] termina aqui. Do ponto de vista conceptual, pouco foi feito: examinámos a ponte objeto-relacional na sua forma mais simples: um objeto @Entity <--> uma tabela. No entanto, este exame permitiu-nos apresentar as ferramentas que utilizaremos ao longo deste documento. Isto permitir-nos-á avançar um pouco mais rapidamente daqui em diante, à medida que examinamos os outros casos da ponte objeto-relacional:

  • à @Entity [Person] anterior, iremos adicionar um campo de endereço modelado por uma classe [Address]. No lado da base de dados, iremos analisar duas implementações possíveis. Os objetos [Person] e [Address] dão origem a
  • uma única tabela [Pessoa] que inclui o endereço
  • duas tabelas [pessoa] e [endereço] ligadas por uma relação de chave estrangeira um-para-um.
  • um exemplo de uma relação um-para-muitos, em que uma tabela [item] está ligada a uma tabela [categoria] através de uma chave estrangeira
  • um exemplo de uma relação muitos-para-muitos em que duas tabelas [Pessoa] e [Atividade] estão ligadas por uma tabela de junção [Pessoa_Atividade].

2.2. Exemplo 2: Relação um-para-um através de uma inclusão

2.2.1. O esquema da base de dados

 
1
2

    eliminar a tabela jpa02_person, se existir;

    criar tabela jpa02_person (
        id bigint not null auto_increment,
        versão inteiro não nulo,
        last_name varchar(30) not null unique,
        nome varchar(30) not null,
        data_de_nascimento data not null,
        casado bit não nulo,
        nbenfants inteiro não nulo,
        endereço1 varchar(30) não nulo,
        endereço2 varchar(30),
        adr3 varchar(30),
        código postal varchar(5) não nulo,
        cidade varchar(20) não nulo,
        código postal varchar(3),
        country varchar(20) not null,
        chave primária (id)
) ENGINE=InnoDB;

  • em [1]: a base de dados (plugin Azurri Clay)
  • em [2]: o DDL gerado pelo Hibernate para o MySQL5

A tabela [jpa02_personne] é a tabela [jpa01_personne] discutida anteriormente, à qual foi adicionado um endereço (linhas 12–18 do DDL).

2.2.2. Os objetos @Entity que representam a base de dados

O endereço de uma pessoa será representado pela seguinte classe [Address]:


package entites;
 
...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {
 
    // fields
    @Column(length = 30, nullable = false)
    private String adr1;
 
    @Column(length = 30)
    private String adr2;
 
    @Column(length = 30)
    private String adr3;
 
    @Column(length = 5, nullable = false)
    private String codePostal;
 
    @Column(length = 20, nullable = false)
    private String ville;
 
    @Column(length = 3)
    private String cedex;
 
    @Column(length = 20, nullable = false)
    private String pays;
 
    // manufacturers
    public Adresse() {
 
    }
 
    public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
    }
 
    // getters and setters
...
 
    // toString
    public String toString() {
        return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
    }
}
  • A principal inovação reside na anotação @Embeddable na linha 5. A classe [Address] não se destina a criar uma tabela, pelo que não possui a anotação @Entity. A anotação @Embeddable indica que a classe se destina a ser incorporada num objeto @Entity e, portanto, na tabela a ele associada. É por isso que, no esquema da base de dados, a classe [Address] não aparece como uma tabela separada, mas como parte da tabela associada à @Entity [Person].

A @Entity [Person] sofreu poucas alterações em relação à sua versão anterior: foi simplesmente adicionado um campo de endereço:


package entites;
 
...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(length = 30, nullable = false)
    private String prenom;
 
    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(nullable = false)
    private boolean marie;
 
    @Column(nullable = false)
    private int nbenfants;
 
    @Embedded
    private Adresse adresse;
 
    // manufacturers
    public Personne() {
    }
...
}
  • A alteração ocorre nas linhas 33–34. O objeto [Person] tem agora um campo de endereço do tipo Address. Isso aplica-se ao POJO. A anotação @Embedded destina-se à ponte objeto-relacional. Indica que o campo [Address address] deve estar encapsulado na mesma tabela que o objeto [Person].

2.2.3. O ambiente de teste

Iremos realizar testes muito semelhantes aos estudados anteriormente. Serão realizados no seguinte contexto:

A implementação utilizada é JPA/Hibernate [6]. O projeto de teste no Eclipse é o seguinte:

O projeto Eclipse [1] difere do anterior apenas no seu código Java [2]. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf e DDL – script Ant) é o já discutido anteriormente, particularmente na Secção 2.1.5. Este continuará a ser o caso para futuros projetos Hibernate e, salvo exceções, não revisitaremos este ambiente. Notavelmente, os ficheiros persistence.xml que configuram a camada JPA/Hibernate para diferentes SGBDs são aqueles já analisados e estão localizados na pasta <conf>.

Se o leitor tiver alguma dúvida sobre os procedimentos a seguir, é encorajado a rever os abordados no estudo anterior.

O projeto Eclipse está disponível [3] na pasta de exemplos [4]. Iremos importá-lo.

2.2.4. Gerar o DDL da base de dados

Seguindo as instruções da Secção 2.1.7, o DDL gerado para o SGBD MySQL 5 é o seguinte:


    drop table if exists jpa02_hb_personne;
 
    create table jpa02_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

O Hibernate reconheceu corretamente que o endereço da pessoa precisava de ser incluído na tabela associada à @Entity Person (linhas 11–17).

2.2.5. InitDB

O código para [InitDB] é o seguinte:


package tests;
...
 
public class InitDB {
 
    // constant
    private final static String TABLE_NAME = "jpa02_hb_personne";
 
    public static void main(String[] args) throws ParseException {
 
        // Persistence context
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // a EntityManager is retrieved from the previous EntityManagerFactory
        em = emf.createEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // request
        Query sql1;
        // delete elements from the PERSONNE table
        sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
        sql1.executeUpdate();
        // creating people
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // address creation
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // associations person <--> address
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // persistence of people
        em.persist(p1);
        em.persist(p2);
        // people display
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // end transaction
        tx.commit();
        // end EntityManager
        em.close();
        // end EntityManagerFactory
        emf.close();
        // log
        System.out.println("terminé...");
 
    }
}

Não há nada de novo neste código. Tudo já foi abordado anteriormente. A execução de [InitDB] com o MySQL5 produz os seguintes resultados:

  • [1]: a saída da consola
  • [2]: a tabela [jpa02_hb_personne] na vista do SQL Explorer
  • [3] e [4]: a sua estrutura e conteúdo.

2.2.6. Main

A classe [Main] é a seguinte:


package tests;
 
...
import entites.Adresse;
import entites.Personne;
 
@SuppressWarnings( { "unused", "unchecked" })
public class Main {
 
    // constant
    private final static String TABLE_NAME = "jpa02_hb_personne";
 
    // Persistence context
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
 
    private static EntityManager em = null;
 
    // shared objects
    private static Personne p1, p2, newp1;
 
    private static Adresse a1, a2, a3, a4, newa1, newa4;
 
    public static void main(String[] args) throws Exception {
        // we retrieve a EntityManager from the EntityManagerFactory
        em = emf.createEntityManager();
 
        // base cleaning
        log("clean");clean();
 
        // dump table
        dumpPersonne();
 
        // test1
        log("test1"); test1();
 
        // test2
        log("test2"); test2();
 
        // test3
        log("test3"); test3();
 
        // test4
        log("test4"); test4();

        // test5
        log("test5");test5();
 
        // fine persistence context
        if (em != null && em.isOpen())
            em.close();
 
        // closure EntityManagerFactory
        emf.close();
    }
 
    // retrieve the current EntityManager
    private static EntityManager getEntityManager() {
...
    }
 
    // pick up a new EntityManager
    private static EntityManager getNewEntityManager() {
...
    }
 
    // display table content Person
    private static void dumpPersonne() {
...
    }
 
    // raz BD
    private static void clean() {
    ...
    }
 
    // logs
    private static void log(String message) {
...
    }
 
    // object creation
    public static void test1() throws ParseException {
        // persistence context
        EntityManager em = getEntityManager();
        // creating people
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // address creation
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // associations person <--> address
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistence of people
        em.persist(p1);
        em.persist(p2);
        // end transaction
        tx.commit();
        // dump
        dumpPersonne();
    }
 
    // modify a context object
    public static void test2() {
        // persistence context
        EntityManager em = getEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // increment the number of children in p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // change your marital status
        p1.setMarie(false);
        // object p1 is automatically saved (dirty checking)
        // at next synchronization (commit or select)
        // end transaction
        tx.commit();
        // the new table is displayed
        dumpPersonne();
    }
 
    // delete an object belonging to the persistence context
    public static void test4() {
        // persistence context
        EntityManager em = getEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // delete attached object p2
        em.remove(p2);
        // end transaction
        tx.commit();
        // the new table is displayed
        dumpPersonne();
    }
 
    // detach, reattach and modify
    public static void test5() {
        // new persistence context
        EntityManager em = getNewEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // reattach p1 to the new context
        p1 = em.find(Personne.class, p1.getId());
        // end transaction
        tx.commit();
        // change p1's address
        p1.getAdresse().setVille("Paris");
        // the new table is displayed
        dumpPersonne();
    }
 
}

Mais uma vez, nada que já não tenhamos visto antes. A saída da consola é a seguinte:

main : ----------- clean
[personnes]
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,0,Martin,Paul,31/01/2000,true,2,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test5
[personnes]
P[1,2,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Paris,null,France]]

O leitor é convidado a estabelecer a ligação entre os resultados e o código.

Estamos agora a utilizar uma implementação JPA / Toplink:

O novo projeto de teste do Eclipse é o seguinte:

O código Java é idêntico ao do projeto Hibernate anterior. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf e ddl – script Ant) é o já discutido na secção 2.1.15.2. Este continuará a ser o caso para futuros projetos Toplink e, salvo exceções, não revisitaremos este ambiente. Em particular, os ficheiros persistence.xml que configuram a camada JPA/Toplink para diferentes DBMS são aqueles já discutidos e localizados na pasta <conf>.

Se o leitor tiver alguma dúvida sobre os procedimentos a seguir, é encorajado a rever os abordados no estudo anterior.

O projeto Eclipse está disponível [3] na pasta examples [4]. Iremos importá-lo.

A execução de [InitDB] com o SGBD MySQL5 produz os seguintes resultados:

  • [1]: a saída da consola
  • [2]: as tabelas [jpa02_tl_personne] e [SEQENCE] na vista do SQL Explorer
  • [3] e [4]: a estrutura e o conteúdo de [jpa02_tl_personne].

Os scripts SQL gerados em ddl/mysql5 [5] são os seguintes:

create.sql


CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.3. Exemplo 3: Relação um-para-um através de uma chave estrangeira

2.3.1. : Esquema da base de dados

1
2

    alter table jpa03_hb_personne
        eliminar
        chave estrangeira FKFBBBFDD05FE379D0;

    DROP TABLE jpa03_hb_adresse IF EXISTS;

    eliminar tabela se existir jpa03_hb_person;

    create table jpa03_hb_address (
        id bigint not null auto_increment,
        versão inteiro não nulo,
        adr1 varchar(30) não nulo,
        adr2 varchar(30),
        adr3 varchar(30),
        zipCode varchar(5) not null,
        cidade varchar(20) não nulo,
        código postal varchar(3),
        country varchar(20) not null,
        chave primária (id)
    ) ENGINE=InnoDB;

    create table jpa03_hb_person (
        id bigint não nulo auto_increment,
        versão inteiro não nulo,
        last_name varchar(30) not null unique,
        nome varchar(30) not null,
        data_de_nascimento data not null,
        casado bit não nulo,
        número_de_filhos inteiro não nulo,
        id_morada bigint não nulo único,
        chave primária (id)
    ) ENGINE=InnoDB;

    alterar tabela jpa03_hb_personne
        add index FKFBBBFDD05FE379D0 (address_id),
        adicionar restrição FKFBBBFDD05FE379D0
        chave estrangeira (address_id)
referências jpa03_hb_adresse (id);
  • em [1]: a base de dados. Desta vez, a morada da pessoa está armazenada numa tabela separada [adresse]. A tabela [personne] está ligada a esta tabela através de uma chave estrangeira.
  • em [2]: o DDL gerado pelo Hibernate para o MySQL5:
    • linhas 9–20: a tabela [address] que será ligada à classe [Address], que se tornou um objeto @Entity.
    • linha 10: a chave primária da tabela [address]
    • linha 30: em vez de um endereço completo, a tabela [person] contém agora o identificador [address_id] para esse endereço.
    • linhas 34–38: `person(address_id)` é uma chave estrangeira em `address(id)`.

2.3.2. Os objetos @Entity que representam a base de dados

Uma pessoa com um endereço é agora representada pela seguinte classe [Person]:


package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{
 
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(length = 30, nullable = false)
    private String prenom;
 
    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(nullable = false)
    private boolean marie;
 
    @Column(nullable = false)
    private int nbenfants;
 
    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;
...
}
  • linhas 32–34: o endereço da pessoa
    • linha 32: a anotação @OneToOne denota uma relação um-para-um: uma pessoa tem pelo menos um e, no máximo, um endereço. O atributo cascade = CascadeType.ALL significa que qualquer operação (persist, merge, remove) na @Entity [Person] deve ser propagada para a @Entity [Address]. Do ponto de vista do contexto de persistência do em, isto significa o seguinte. Se p é uma pessoa e tem um endereço:
      • uma operação explícita em.persist(p) irá desencadear uma operação implícita em.persist(a)
      • uma operação explícita em.merge(p) irá desencadear uma operação implícita em.merge(a)
      • uma operação explícita em.remove(p) irá desencadear uma operação implícita em.remove(a)

A experiência mostra que estas cascatas implícitas não são uma panaceia. Os programadores acabam por esquecer o que elas fazem. Operações explícitas no código podem ser preferíveis. Existem diferentes tipos de cascatas. A anotação @OneToOne poderia ter sido escrita da seguinte forma:


//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)

O atributo cascade aceita como valor uma matriz de constantes que especificam os tipos de cascata desejados.

O atributo fetch=FetchType.LAZY instrui o Hibernate a carregar a dependência no último momento possível. Ao adicionar uma lista de pessoas ao contexto de persistência, pode não ser necessário incluir os seus endereços. Por exemplo, pode querer apenas o endereço de uma pessoa específica selecionada por um utilizador através de uma interface web. O atributo fetch=FetchType.EAGER, por outro lado, solicita que as dependências sejam carregadas imediatamente.

  • (continuação)
    • linha 33: a anotação @JoinColumn define a chave estrangeira que a tabela @Entity [Person] possui na tabela @Entity [Address]. O atributo name define o nome da coluna que serve como chave estrangeira. O atributo unique=true impõe uma relação um-para-um: o mesmo valor não pode aparecer duas vezes na coluna [address_id]. O atributo nullable=false impõe que uma pessoa deve ter um endereço.

O endereço de uma pessoa é agora representado pela seguinte @Entity [Address]:


package entites;
 
...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {
 
    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false)
    private String adr1;
 
    @Column(length = 30)
    private String adr2;
 
    @Column(length = 30)
    private String adr3;
 
    @Column(length = 5, nullable = false)
    private String codePostal;
 
    @Column(length = 20, nullable = false)
    private String ville;
 
    @Column(length = 3)
    private String cedex;
 
    @Column(length = 20, nullable = false)
    private String pays;
 
    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
    private Personne personne;
 
    // manufacturers
    public Adresse() {
 
    }
...
}
  • linha 4: a classe [Address] torna-se um objeto @Entity. Será, portanto, o objeto de uma tabela na base de dados.
  • linhas 9–12: Tal como qualquer objeto @Entity, [Address] possui uma chave primária. Esta foi denominada Id e possui as mesmas anotações (padrão) que a chave primária Id da @Entity [Person].
  • linhas 39–40: a relação um-para-um com a @Entity [Person]. Há aqui várias subtilezas:
    • Em primeiro lugar, o campo `person` não é obrigatório. Permite-nos utilizar um endereço para identificar a única pessoa associada a esse endereço. Se não quiséssemos esta funcionalidade, o campo `person` não existiria e tudo continuaria a funcionar.
    • A relação um-para-um que liga as duas entidades [Person] e [Address] já foi configurada na @Entity [Person]:

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;

Para evitar que as duas configurações um-para-um entrem em conflito entre si, uma é considerada primária e a outra inversa. É a relação primária que é gerida pela ponte objeto-relacional. A outra relação, conhecida como relação inversa, não é gerida diretamente: é gerida indiretamente através da relação primária. Em @Entity [Address]:


@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

é o atributo mappedBy que torna a relação um-para-um acima a relação inversa da relação um-para-um primária definida pelo campo address da @Entity [Person].

2.3.3. O projeto Eclipse / Hibernate 1

A implementação JPA utilizada aqui é o Hibernate. O projeto de teste do Eclipse é o seguinte:

O projeto está localizado [3] na pasta de exemplos [4]. Vamos importá-lo.

2.3.4. Geração do DDL da base de dados

Seguindo as instruções da Secção 2.1.7, o DDL obtido para o SGBD MySQL 5 é o que se encontra no início desta secção.

2.3.5. InitDB

O código para [InitDB] é o seguinte:


package tests;
...
import entites.Adresse;
import entites.Personne;
 
public class InitDB {
 
    // constant
    private final static String TABLE_PERSONNE = "jpa03_hb_personne";

    private final static String TABLE_ADRESSE = "jpa03_hb_adresse";
 
    public static void main(String[] args) throws ParseException {
        // Persistence context
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // a EntityManager is retrieved from the previous EntityManagerFactory
        em = emf.createEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // request
        Query sql1;
        // delete elements from the PERSONNE table
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // delete elements from the ADRESSE table
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // creating people
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // address creation
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // associations person <--> address
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // persistence of persons and cascading of their addresses
        em.persist(p1);
        em.persist(p2);
        // and a3 and a4 addresses not linked to persons
        em.persist(a3);
        em.persist(a4);
        // people display
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // address display
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
 
        // end transaction
        tx.commit();
        // end EntityManager
        em.close();
        // end EntityManagerFactory
        emf.close();
        // log
        System.out.println("terminé...");
 
    }
}

Iremos apenas comentar o que há de novo em relação ao que já foi abordado:

  • linhas 31–32: criamos duas pessoas
  • linhas 34–37: criamos quatro endereços
  • linhas 39-42: associamos as pessoas (p1, p2) aos endereços (a1, a2). Os endereços (a3, a4) ficam órfãos. Nenhuma pessoa os referencia. O DDL permite isso. Embora uma pessoa deva ter um endereço, o inverso não é verdadeiro.
  • linhas 44-45: persistimos as pessoas (p1, p2). Uma vez que definimos o atributo cascade como CascadeType.ALL na relação um-para-um que liga uma pessoa ao seu endereço, os endereços (a1, a2) destas duas pessoas também devem ser persistidos. É isto que queremos verificar. Para os endereços órfãos (a3, a4), temos de fazer isto explicitamente (linhas 47–48).
  • linhas 51–53: exibição da tabela de pessoas
  • Linhas 56–57: Exibição da tabela de endereços

A execução de [InitDB] com o MySQL5 produz os seguintes resultados:

  • [1]: a saída da consola
  • [2]: as tabelas [jpa03_hb_*] na vista do SQL Explorer
  • [3]: a tabela people
  • [4]: a tabela de endereços. Estão todas lá. Repare também na relação entre a coluna [adresse_id] em [3] e a coluna [id] em [4] (chave estrangeira).

2.3.6. Main

A classe [Main] executa seis testes, que iremos analisar.

2.3.6.1. Teste1

Este teste é o seguinte:


// création d'objets
    public static void test1() throws ParseException {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // création personnes
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // création adresses
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // associations personne <--> adresse
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistance des personnes
        em.persist(p1);
        em.persist(p2);
        // et des adresses a3 et a4 non liées à des personnes
        em.persist(a3);
        em.persist(a4);
        // fin transaction
        tx.commit();
        // on affiche les tables
        dumpPersonne();
        dumpAdresse();
    }

Este código foi retirado de [InitDB]. O resultado é o seguinte:

1
2
3
4
5
6
7
8
9
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]

Ambas as tabelas foram preenchidas.

2.3.6.2. Teste2

Este teste é o seguinte:


    // modifier un objet du contexte
    public static void test2() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on incrémente le nbre d'enfants de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // on modifie son état marital
        p1.setMarie(false);
        // l'objet p1 est automatiquement sauvegardé (dirty checking)
        // lors de la prochaine synchronisation (commit ou select)
        // fin transaction
        tx.commit();
        // on affiche la nouvelle table
        dumpPersonne();
}

O resultado é o seguinte:

1
2
3
4
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
  • linha 4: a pessoa p1 viu o seu número de filhos aumentar em 1 e a sua versão mudar de 0 para 1

2.3.6.3. Teste 4

Este teste é o seguinte:


    // supprimer un objet appartenant au contexte de persistance
    public static void test4() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on supprime l'objet attaché p2
        em.remove(p2);
        // fin transaction
        tx.commit();
        // on affiche les nouvelles tables
        dumpPersonne();
        dumpAdresse();
}
  • Linha 9: Removemos a pessoa p2. Esta pessoa tem uma relação em cascata com o endereço a2. Por conseguinte, o endereço a2 também deve ser removido.

O resultado do teste 4 é o seguinte:

main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • A pessoa p2 que aparece na linha 3 do teste 1 já não está presente no teste 4
  • O mesmo se aplica ao seu endereço a2, que aparece na linha 7 do teste 1, mas está ausente do teste 4.

2.3.6.4. Teste 5

Este teste é o seguinte:


// détacher, réattacher et modifier
    public static void test5() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on réattache p1 au nouveau contexte
        p1 = em.find(Personne.class, p1.getId());
        // on change l'adresse de p1
        p1.getAdresse().setVille("Paris");
        // fin transaction
        tx.commit();
        // on affiche les nouvelles tables
        dumpPersonne();
        dumpAdresse();
    }
  • Linha 4: Temos um novo contexto de persistência, por isso está vazio.
  • linha 9: adicionamos a pessoa p1 a ele. p1 é recuperada da base de dados porque não está no contexto. Os elementos dependentes de p1 (o seu endereço) não são recuperados da base de dados porque escrevemos:

    @OneToOne(..., fetch=FetchType.LAZY)

Este é o conceito de «carregamento preguiçoso»: as dependências de um objeto persistente só são carregadas na memória quando são necessárias.

  • Linha 11: Modificamos o campo cidade do endereço de p1. Devido à chamada getAddress, e se o endereço de p1 ainda não estivesse no contexto de persistência, ele será recuperado da base de dados.
  • Linha 13: Confirmamos a transação, o que sincronizará o contexto de persistência com a base de dados. A base de dados irá detetar que o endereço da pessoa p1 foi modificado e irá guardá-lo.

A execução do test5 produz os seguintes resultados:

main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • A pessoa p1 (linha 3 do teste 4, linha 10 do teste 5) observou corretamente a mudança da sua cidade de Angers (linha 5 do teste 4) para Paris (linha 12 do teste 5).

2.3.6.5. Teste6

Este teste é o seguinte:


// delete an Address object
    public static void test6() {
        EntityTransaction tx = null;
        // new persistence context
        EntityManager em = getNewEntityManager();
        // start of transaction
        tx = em.getTransaction();
        tx.begin();
        // reattach address a3 to new context
        a3 = em.find(Adresse.class, a3.getId());
        System.out.println(a3);
        // we delete it
        em.remove(a3);
        // end transaction
        tx.commit();
        // dump table Address
        dumpAdresse();
    }
  • Linha 5: Estamos num novo contexto de persistência, por isso está vazio.
  • Linha 10: Colocamos o endereço a3 no contexto de persistência
  • linha 13: eliminamo-lo. Era um endereço órfão (não associado a uma pessoa). A eliminação é, portanto, possível.

O resultado da execução é o seguinte:

main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • O endereço a3 do teste 5 (linha 6) desapareceu dos endereços do teste 6 (linhas 11-12)

2.3.6.6. Teste 7

Este teste é o seguinte:


// rollback
    public static void test7() {
        EntityTransaction tx = null;
        try {
            // nouveau contexte de persistance
            EntityManager em = getNewEntityManager();
            // début transaction
            tx = em.getTransaction();
            tx.begin();
            // on réattache l'adresse a1 au nouveau contexte
            newa1 = em.find(Adresse.class, a1.getId());
            // on réattache l'adresse a4 au nouveau contexte
            newa4 = em.find(Adresse.class, a4.getId());
            // on essaie de les supprimer - devrait lancer une exception car on ne peut supprimer une adresse liée à une personne, ce qui est le cas de newa1
            em.remove(newa4);
            em.remove(newa1);
            // fin transaction
            tx.commit();
        } catch (RuntimeException e1) {
            // on a eu un pb
            System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
                    .getCause());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // on abandonne le contexte courant
            em.clear();
        }
        // dump - la table Adresse n'a pas du changer à cause du rollback
        dumpAdresse();
    }
  • test7: a testar um rollback de transação
    • linha 6: estamos num novo contexto de persistência, por isso está vazio.
    • linha 11: colocamos o endereço a1 no contexto de persistência, sob a referência newa1
    • linha 13: colocamos o endereço a4 no contexto de persistência, sob a referência newa4
    • linhas 15-16: eliminamos os dois endereços newa1 e newa4. newa1 é o endereço da pessoa p1 e, por isso, é referenciado por p1 na base de dados através de uma chave estrangeira. A eliminação de newa1 irá, portanto, falhar e lançar uma exceção quando o contexto de persistência for sincronizado após a confirmação da transação (linha 18). A transação será revertida (linha 25) e, assim, ambas as operações na transação serão canceladas. Devemos, portanto, observar que o endereço newa4, que poderia ter sido legalmente eliminado, não foi eliminado.

A execução produz o seguinte resultado:


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • A tabela de endereços no Teste 7 (linhas 12–13) é idêntica à do Teste 6 (linhas 4–5). Parece que a reversão ocorreu. Dito isto, a mensagem de erro na linha 9 é um mistério e justifica uma investigação mais aprofundada. Parece que a exceção que ocorreu não é a esperada. Precisamos de definir os registos do Hibernate para o modo DEBUG no ficheiro log4j.properties para obtermos uma imagem mais clara:

# Root logger option
log4j.rootLogger=ERROR, stdout
 
# Hibernate logging options (INFO only shows startup messages)
log4j.logger.org.hibernate=DEBUG

Podemos então ver que, quando o endereço a1 foi colocado no contexto de persistência, o Hibernate também colocou a pessoa p1 nesse contexto, provavelmente devido à relação um-para-um da @Entity [Address]:


    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

Embora tenha sido solicitado "LazyLoading" aqui, a dependência [Person] é, no entanto, carregada imediatamente. Isto significa provavelmente que o atributo fetch=FetchType.LAZY não tem efeito aqui. Observamos então que, ao confirmar a transação, o Hibernate preparou a eliminação dos endereços a1 e a4, bem como o salvamento da pessoa p1. E é aqui que ocorre a exceção: como a pessoa p1 tem uma cascata no seu endereço, o Hibernate também pretende persistir o endereço a1, apesar de este ter acabado de ser eliminado. É o Hibernate que lança a exceção, não o controlador JDBC. Daí a mensagem na linha 9 acima. Além disso, podemos ver que o rollback na linha 25 nunca é executado porque a transação se tornou inativa. O teste na linha 24, portanto, impede o rollback.

Não alcançámos, portanto, o objetivo pretendido: demonstrar um rollback. Na verdade, nunca foram emitidas instruções SQL para a base de dados. Vamos destacar alguns pontos-chave:

  • a importância de ativar o registo detalhado para compreender o que o ORM está a fazer
  • embora um ORM possa facilitar a vida de um programador, também pode complicá-la ao ocultar comportamentos que o programador precisa de conhecer. Neste caso, a forma como as dependências de um @Entity são carregadas.

2.3.7. Projeto Eclipse / Hibernate 2

Copiamos e colamos o projeto Eclipse/Hibernate para fazer algumas pequenas alterações na configuração dos objetos @Entity:

O projeto encontra-se [3] na pasta de exemplos [4]. Vamos importá-lo.

Modificamos apenas a @Entity [Address] para que deixe de ter uma relação inversa um-para-um com a @Entity [Person]:


package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {
 
    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false)
    private String adr1;
 
    ...
 
    @Column(length = 20, nullable = false)
    private String pays;
 
//    @OneToOne(mappedBy = "address", fetch=FetchType.LAZY)
//    private Person person;
 
    // manufacturers
    public Adresse() {
 
    }
  • linhas 25-26: a relação inversa @OneToOne foi removida. É importante compreender que uma relação inversa nunca é essencial. Apenas a relação primária o é. A relação inversa pode ser utilizada por conveniência. Aqui, proporcionou uma forma simples de recuperar o proprietário de um endereço. Uma relação inversa pode sempre ser substituída por uma consulta JPQL. É isso que iremos demonstrar no exemplo seguinte.

Os programas de teste são idênticos. O que nos interessa é apenas o Teste 7, aquele em que vimos a relação inversa um-para-um em ação. Estamos também a adicionar o Teste 8 para mostrar como, sem a relação inversa Endereço -> Pessoa, ainda podemos recuperar a pessoa com um determinado endereço.

O Teste 7 permanece inalterado. A sua execução produz agora os seguintes resultados (registos desativados):


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • Desta vez, obtemos a exceção esperada: aquela lançada pelo controlador JDBC porque tentámos eliminar uma linha na tabela [endereço] que é referenciada por uma chave estrangeira de uma linha na tabela [pessoa]. A linha [10] explica claramente a causa do erro.
  • O rollback ocorreu de facto: no final do teste 7, a tabela [address] (linhas 12–13) está igual ao que estava no final do teste 6 (linhas 4–5).

Qual é a diferença em relação ao Teste 7 do projeto Eclipse anterior? Por que é que aqui obtemos uma exceção Jdbc que não ocorreu no teste anterior? Porque a @Entity [Address] já não tem uma relação inversa um-para-um com a @Entity [Person]; é gerida de forma independente pelo Hibernate. Quando o endereço newa1 foi introduzido no contexto de persistência, o Hibernate não colocou também a pessoa p1 com esse endereço nesse contexto. A eliminação dos endereços newa1 e newa4 ocorreu, portanto, sem quaisquer entidades Person no contexto.

Agora, como poderíamos usar o endereço newa1 para encontrar a pessoa p1 com esse endereço? Essa é uma pergunta legítima. O Teste 8 a seguir responde a ela:


// relation inverse un-à-un
    // réalisée par une requête JPQL
    public static void test8() {
        EntityTransaction tx = null;
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        tx = em.getTransaction();
        tx.begin();
        // on réattache l'adresse a1 au nouveau contexte
        newa1 = em.find(Adresse.class, a1.getId());
        // on récupère la personne propriétaire de cette adresse
        Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
                .getSingleResult();
        // on les affiche
        System.out.println("adresse=" + newa1);
        System.out.println("personne=" + p1);
        // fin transaction
        tx.commit();
    }
  • linha 6: novo contexto de persistência vazio
  • linhas 8-9: iniciar transação
  • linha 11: o endereço a1 é introduzido no contexto de persistência e referenciado por newa1.
  • linha 13: a pessoa p1 com o endereço newa1 é recuperada através de uma consulta JPQL. Sabemos que [Person] e [Address] estão ligadas por uma relação de chave estrangeira. Na classe [Person], é o campo [address] que tem a anotação @OneToOne, que define esta relação. A instrução JPQL «select p from Person p join p.address a» realiza uma junção entre as tabelas [Person] e [Address]. O SQL equivalente gerado numa consola Hibernate (ver exemplos na secção 2.1.12) é o seguinte:
SQL #0 types: entites.Personne
-----------------
select
  personne0_.id as id1_,
  personne0_.version as version1_,
  personne0_.nom as nom1_,
  personne0_.prenom as prenom1_,
  personne0_.datenaissance as datenais5_1_,
  personne0_.marie as marie1_,
  personne0_.nbenfants as nbenfants1_,
  personne0_.adresse_id as adresse8_1_ 
 from
  jpa04_hb_personne personne0_ 
 inner join
  jpa04_hb_adresse adresse1_ 
on personne0_.adresse_id=adresse1_.id

A junção entre as duas tabelas é claramente visível. Cada pessoa está agora ligada ao seu endereço. Resta especificar que estamos apenas interessados no endereço newa1. A consulta passa a ser "select p from Person p join p.address a where a.id=:addressId". Note-se o uso dos aliases p e a. As consultas JPQL fazem uso extensivo de aliases. Assim, a expressão "from Person p join p.address a" significa que uma pessoa é representada pelo alias p e o seu endereço (p.address) pelo alias a. A operação de restrição "where a.id=:adresseId" limita as linhas solicitadas apenas às pessoas p cujo endereço a tem o valor :adresseId como seu identificador. :adresseId é chamado de parâmetro, e a consulta JPQL é uma consulta JPQL parametrizada. Em tempo de execução, deve ser atribuído um valor a este parâmetro. Isto é feito utilizando o método

Query setParameter(String nomParamètre, Object valeurParamètre)

, que permite atribuir um valor a um parâmetro identificado pelo seu nome. Note-se que setParameter devolve um objeto Query, tal como o método createQuery. Isto significa que é possível encadear chamadas de métodos [por exemplo, createQuery(...).setParameter(...).getSingleResult(...)], uma vez que os métodos [setParameter, getSingleResult] são métodos da interface Query. O método [getSingleResult] é utilizado para consultas Select que devolvem apenas um único resultado. É o que acontece neste caso.

  • Linhas 16–17: Exibimos o endereço newa1 e a pessoa p1 associada a esse endereço, para verificação.

O resultado obtido é o seguinte:

1
2
3
main : ----------- test8
adresse=A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
personne=P[1,1,Martin,Paul,31/01/2000,false,3,1]

Está correto. Podemos concluir deste exemplo que a relação inversa um-para-um da @entidade [Endereço] para a @entidade [Pessoa] não era essencial. A experiência demonstrou aqui que a sua remoção resultou num comportamento mais previsível do código. Este é frequentemente o caso.

2.3.8. Console do Hibernate

O Teste 8 anterior utilizou um comando JPQL para realizar uma junção entre as entidades Pessoa e Endereço. Embora semelhantes ao SQL, o JPQL da JPA e o HQL do Hibernate requerem aprendizagem, e a consola do Hibernate é excelente para este fim. Já a utilizámos na Secção 2.1.12 para consultar uma única tabela. Vamos fazê-lo novamente aqui para consultar duas tabelas ligadas por uma relação de chave estrangeira.

Vamos criar uma consola Hibernate para o nosso projeto Eclipse atual:

  • [1]: Mude para a perspetiva [Hibernate Console] (Janela / Abrir Perspetiva / Outros)
  • [2]: Criamos uma nova configuração
  • usando o botão [4], selecionamos o projeto Java para o qual a configuração do Hibernate está a ser criada. O seu nome aparece em [3].
  • Em [5], introduzimos o nome que pretendemos para esta configuração. Aqui, utilizámos o nome do projeto Java.
  • Em [6], especificamos que estamos a utilizar uma configuração JPA para que a ferramenta saiba que deve utilizar o ficheiro [META-INF/persistence.xml]
  • Em [7], especificamos no ficheiro [META-INF/persistence.xml] que deve ser utilizada a unidade de persistência denominada jpa.
  • Em [8], validamos a configuração.

Em seguida, o SGBD deve ser iniciado. Neste caso, trata-se do MySQL 5.

  • Em [1]: A configuração criada tem uma estrutura em árvore com três ramos
  • em [2]: o ramo [Configuration] lista os objetos que a consola utilizou para se configurar: neste caso, @Entity Person e Address.
  • Em [3]: A Session Factory é um conceito do Hibernate semelhante ao EntityManager do JPA. Ela faz a ponte entre o mundo dos objetos e o relacional utilizando objetos do ramo [Configuration]. [3] apresenta os objetos do contexto de persistência, neste caso as entidades @Entity Person e Address.
  • em [4]: a base de dados acedida através da configuração encontrada em [persistence.xml]. Aqui encontramos as tabelas [jpa04_hb_*] geradas pelo nosso projeto Eclipse atual.
  • Em [1], criamos um editor HQL
  • no editor HQL,
    • em [2], selecionamos a configuração do Hibernate a utilizar, caso existam várias (o que é o caso aqui)
    • em [3], digite o comando JPQL que deseja executar; aqui, o comando JPQL do Teste 8
    • em [4], executamo-lo
    • Em [5], obtém os resultados da consulta na janela [Hibernate Query Result].
    • Em [6], a janela [Hibernate Dynamic SQL preview] permite-lhe visualizar a consulta SQL que foi executada.

Outra forma de obter o mesmo resultado:

  • Em [1]: o comando JPQL que realiza a junção entre as entidades Pessoa e Endereço. [ref1] refere-se a esta forma como uma «junção theta».
  • em [2]: o equivalente em SQL
  • Em [3]: o resultado

Uma terceira forma aceite apenas pelo Hibernate (HQL):

  • em [1]: a consulta HQL. O JPQL não aceita a notação p.address.id. Aceita apenas um nível de indireção.
  • em [2]: o equivalente em SQL. Note-se que evita a junção de tabelas.
  • em [3]: o resultado

Aqui estão alguns outros exemplos:

  • em [1]: a lista de pessoas com os seus endereços
  • em [2]: o equivalente em SQL.
  • em [3]: o resultado
  • em [1]: a lista de endereços com o seu proprietário, se houver, ou nenhum caso contrário (junção externa à direita: a entidade Endereço, que fornecerá as linhas não relacionadas com Pessoa, está à direita da palavra-chave join).
  • em [2]: o equivalente em SQL.
  • em [3]: o resultado

Note que apenas a entidade Pessoa tem uma relação com a entidade Endereço. O inverso já não é verdadeiro, uma vez que removemos a relação inversa um-para-um chamada Pessoa na entidade Endereço. Se esta relação inversa existisse, poderíamos ter escrito:

  • em [1]: a lista de endereços com o respetivo proprietário, se houver, ou nenhum caso contrário (junção externa à esquerda: a entidade Endereço, que irá devolver linhas sem relação com Pessoa, encontra-se no lado esquerdo da palavra-chave join).
  • em [2]: o equivalente em SQL.
  • em [3]: o resultado

Recomendamos vivamente ao leitor que pratique a linguagem JPQL utilizando a consola do Hibernate.

Estamos agora a utilizar uma implementação JPA / Toplink:

O novo projeto de teste do Eclipse é o seguinte:

O código Java é idêntico ao do projeto Hibernate anterior. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf e ddl – script Ant) é o discutido na secção 2.1.15.2. O projeto Eclipse está disponível [3] na pasta de exemplos [4]. Vamos importá-lo.

O ficheiro <persistence.xml> é modificado num único ponto, especificamente nas entidades declaradas:


    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classes persistantes -->
        <class>entites.Personne</class>
        <class>entites.Adresse</class>
        <!-- propriétés de l'unité de persistance -->
...
  • linhas 5 e 6: as duas entidades geridas

A execução de [InitDB] com o SGBD MySQL5 produz os seguintes resultados:

Em [1], a saída da consola; em [2], as duas tabelas [jpa04_tl] geradas; em [3], os scripts SQL gerados. O seu conteúdo é o seguinte:

create.sql


CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.4. Exemplo 4: Relação um-para-muitos

2.4.1. O esquema da base de dados

1
2

    alter table jpa06_article
        eliminar
        chave estrangeira FKFFBDD9D8ECCE8750;

    ELIMINAR TABELA jpa06_article SE EXISTIR;

    eliminar tabela se existir jpa06_category;

    create table jpa06_article (
        id bigint not null auto_increment,
        versão inteiro não nulo,
        nome varchar(30),
        category_id bigint not null,
        chave primária (id)
    ) ENGINE=InnoDB;

    create table jpa06_category (
        id bigint not null auto_increment,
        versão inteiro não nulo,
        nome varchar(30),
        chave primária (id)
    ) ENGINE=InnoDB;

    ALTER TABLE jpa06_article
        add index FKFFBDD9D8ECCE8750 (category_id),
        adicionar restrição FKFFBDD9D8ECCE8750
        chave estrangeira (category_id)
referências jpa06_categorie (id);
  • em [1], a base de dados, e em [2], o seu DDL (MySQL5)

Um artigo A(id, versão, nome) pertence a exatamente uma categoria C(id, versão, nome). Uma categoria C pode conter 0, 1 ou mais artigos. Temos uma relação um-para-muitos (Categoria -> Artigo) e a relação inversa muitos-para-um (Artigo -> Categoria). Esta relação é representada pela chave estrangeira que a tabela [artigo] possui na tabela [categoria] (linhas 24–28 do DDL).

2.4.2. Os objetos @Entity que representam a base de dados

Um artigo é representado pela seguinte @Entity [Artigo]:


package entites;
 
...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {
 
    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @SuppressWarnings("unused")
    @Version
    private int version;
 
    @Column(length = 30)
    private String nom;
 
    // main relationship Article (many) -> Category (one)
    // implemented by a foreign key (categorie_id) in Article
    // 1 Article must have 1 Category (nullable=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;
 
    // manufacturers
    public Article() {
    }
 
    // getters and setters
    ...
    // toString
    public String toString() {
        return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
    }
 
}
  • linhas 9-11: chave primária da @Entity
  • linhas 13-15: o seu número de versão
  • linhas 17-18: nome do artigo
  • linhas 20-25: relação muitos-para-um que liga a @Entity Article à @Entity Category:
    • linha 23: a anotação ManyToOne. O Many refere-se à @Entity Article na qual nos encontramos, e o One refere-se à @Entity Category (linha 25). Uma categoria (One) pode ter vários artigos (Many).
    • linha 24: a anotação ManyToOne define a coluna de chave estrangeira na tabela [article]. Ela será denominada (name) categorie_id, e cada linha deve ter um valor nesta coluna (nullable=false).
    • Linha 25: A categoria à qual o artigo pertence. Quando um artigo é adicionado ao contexto de persistência, solicitamos que a sua categoria não seja adicionada imediatamente (fetch=FetchType.LAZY, linha 23). Não sabemos se este pedido faz sentido. Veremos.

Uma categoria é representada pela seguinte @Entity [Category]:


package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
 
    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @SuppressWarnings("unused")
    @Version
    private int version;
 
    @Column(length = 30)
    private String nom;
 
    // inverse relationship Category (one) -> Article (many) from relationship Article (many) -> Category (one)
    // cascade insertion Category -> insertion Articles
    // cascade maj Category -> maj Articles
    // cascade delete Category -> delete Articles
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();
 
    // manufacturers
    public Categorie() {
    }
 
    // getters and setters
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }
 
    // bidirectional association Category <--> Article
    public void addArticle(Article article) {
        // the item is added to the collection of items in the category
        articles.add(article);
        // article changes category
        article.setCategorie(this);
    }
}
  • linhas 8-11: a chave primária da @Entity
  • linhas 12-14: a sua versão
  • linhas 16-17: o nome da categoria
  • linhas 19-24: o conjunto de itens da categoria
    • Linha 23: A anotação @OneToMany denota uma relação um-para-muitos. O «One» refere-se à @Entity [Category] em que nos encontramos atualmente, e o «Many» refere-se ao tipo [Article] na linha 24: uma (One) categoria tem muitos (Many) artigos.
    • Linha 23: A anotação é o inverso (mappedBy) da anotação ManyToOne colocada no campo category da @Entity Article: mappedBy=category. A relação ManyToOne colocada no campo category da @Entity Article é a relação primária. É essencial. Implementa a relação de chave estrangeira que liga a @Entity Article à @Entity Category. A relação OneToMany colocada no campo articles da @Entity Category é a relação inversa. Não é essencial. É uma facilidade para recuperar os artigos de uma categoria. Sem esta facilidade, estes artigos seriam recuperados através de uma consulta JPQL.
    • Linha 23: `cascadeType.ALL` especifica que as operações (persist, merge, remove) realizadas numa `@Entity Category` devem propagar-se aos seus artigos.
    • Linha 24: Os artigos de uma categoria serão colocados num objeto do tipo `Set<Article>`. O tipo `Set` não permite duplicados. Assim, o mesmo artigo não pode ser adicionado duas vezes ao objeto `Set<Article>`. O que significa «o mesmo artigo»? Para indicar que o artigo `a` é igual ao artigo `b`, o Java utiliza a expressão `a.equals(b)`. Na classe Object, a classe pai de todas as classes, a.equals(b) é verdadeiro se a==b, ou seja, se os objetos a e b tiverem a mesma localização na memória. Pode-se querer dizer que os itens a e b são iguais se tiverem o mesmo nome. Neste caso, o programador deve redefinir dois métodos na classe [Item]:
      • equals: que deve devolver true se os dois itens tiverem o mesmo nome
      • hashCode: deve devolver um valor inteiro idêntico para dois objetos [Article] que o método equals considere iguais. Aqui, o valor será, portanto, construído a partir do nome do artigo. O valor devolvido por hashCode pode ser qualquer inteiro. É utilizado em vários contentores de objetos, nomeadamente dicionários (Hashtable).

A relação OneToMany pode utilizar outros tipos além de Set para armazenar o Many, tais como objetos List. Não abordaremos estes casos neste documento. O leitor pode encontrá-los em [ref1].

  • Linha 38: O método [addArticle] permite-nos adicionar um artigo a uma categoria. O método garante que ambas as extremidades da relação OneToMany que liga [Category] a [Article] sejam atualizadas.

2.4.3. O Projeto Eclipse / Hibernate 1

A implementação JPA utilizada aqui é o Hibernate. O projeto de teste do Eclipse é o seguinte:

O projeto encontra-se [3] na pasta de exemplos [4]. Vamos importá-lo.

2.4.4. Geração do DDL da base de dados

Seguindo as instruções da Secção 2.1.7, o DDL gerado para o SGBD MySQL 5 é o que se encontra no início deste exemplo, na Secção 2.4.1.

2.4.5. InitDB

O código para [InitDB] é o seguinte:


package tests;
 
...
public class InitDB {
 
    // constant
    private final static String TABLE_ARTICLE = "jpa05_hb_article";
 
    private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";
 
    public static void main(String[] args) {
        // Persistence context
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // a EntityManager is retrieved from the previous EntityManagerFactory
        em = emf.createEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // request
        Query sql1;
        // delete elements from the ARTICLE table
        sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
        sql1.executeUpdate();
        // delete elements from the CATEGORIE table
        sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
        sql1.executeUpdate();
        // create three categories
        Categorie categorieA = new Categorie();
        categorieA.setNom("A");
        Categorie categorieB = new Categorie();
        categorieB.setNom("B");
        Categorie categorieC = new Categorie();
        categorieC.setNom("C");
        // create 3 items
        Article articleA1 = new Article();
        articleA1.setNom("A1");
        Article articleA2 = new Article();
        articleA2.setNom("A2");
        Article articleB1 = new Article();
        articleB1.setNom("B1");
        // link them to their category
        categorieA.addArticle(articleA1);
        categorieA.addArticle(articleA2);
        categorieB.addArticle(articleB1);
        // persist categories and cascade (insert) articles
        em.persist(categorieA);
        em.persist(categorieB);
        em.persist(categorieC);
        // category display
        System.out.println("[categories]");
        for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
            System.out.println(p);
        }
        // item display
        System.out.println("[articles]");
        for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
            System.out.println(p);
        }
        // end transaction
        tx.commit();
        // end EntityManager
        em.close();
        // end EntityMangerFactory
        emf.close();
        // log
        System.out.println("terminé...");
 
    }
}
  • linhas 22-27: as tabelas [article] e [category] são esvaziadas. Note que devemos começar pela tabela que contém a chave estrangeira. Se começássemos pela tabela [category], eliminaríamos categorias referenciadas por linhas na tabela [article], e o SGBD rejeitaria isso.
  • linhas 29-34: criamos três categorias A, B, C
  • Linhas 36–41: Criamos três artigos: A1, A2 e B1 (a letra indica a categoria)
  • Linhas 43–45: Os três artigos são colocados nas respetivas categorias
  • Linhas 47–49: As três categorias são colocadas no contexto de persistência. Devido à cascata Categoria → Artigo, os artigos associados a elas também serão colocados lá. Assim, todos os objetos criados estão agora no contexto de persistência.
  • Linhas 50-59: O contexto de persistência é consultado para obter a lista de categorias e itens. Sabemos que isto irá desencadear uma sincronização do contexto com a base de dados. É neste momento que as categorias e os itens serão guardados nas respetivas tabelas.

A execução de [InitDB] com o MySQL5 produz os seguintes resultados:

  • [1]: a saída da consola
  • [2]: as tabelas [jpa05_hb_*] na vista do SQL Explorer
  • [3]: a tabela categories
  • [4]: a tabela de artigos. Repare na relação entre [categorie_id] em [4] e [id] em [3] (chave estrangeira).

2.4.6. Main

A classe [Main] executa uma série de testes que analisamos, exceto os testes 1 e 2, que utilizam o código de [InitDB] para inicializar a base de dados.

2.4.6.1. Teste 3

Este teste é o seguinte:


    // search for a particular item
    public static void test3() {
        // new persistence context
        EntityManager em = getNewEntityManager();
        // transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // loading category
        Categorie categorie = em.find(Categorie.class, categorieA.getId());
        // category display and related articles
        System.out.format("Articles de la catégorie %s :%n", categorie);
        for (Article a : categorie.getArticles()) {
            System.out.println(a);
        }
        // end transaction
        tx.commit();
}
  • linha 4: temos um novo contexto de persistência, por isso está vazio
  • linhas 6-7: iniciar transação
  • linha 9: a categoria A é recuperada da base de dados para o contexto de persistência
  • linha 11: exibimos a categoria A
  • linhas 12–14: exibimos os itens da categoria A. Isto demonstra a vantagem da relação inversa OneToMany para a @Entity Category. A sua presença evita que tenhamos de fazer uma consulta JPQL para recuperar os itens da categoria A. Para os obter, utilizamos o método get do campo items.

Os resultados são os seguintes:

main : ----------- test1
[categories]
Categorie[1,0,A]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[1,0,A1,1]
Article[2,0,A2,1]
Article[3,0,B1,2]
main : ----------- test2
3 categorie(s) trouvée(s) :
A
B
C
3 article(s) trouvé(s) :
A1
A2
B1
main : ----------- test3
Articles de la catégorie Categorie[1,0,A] :
Article[2,0,A2,1]
Article[1,0,A1,1]
  • linha 20: categoria A
  • Linhas 21-22: os dois itens da categoria A

2.4.6.2. Teste 4

Este teste é o seguinte:


    // supprimer un article
    @SuppressWarnings("unchecked")
    public static void test4() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // chargement article A1
        Article newarticle1 = em.find(Article.class, articleA1.getId());
        // suppression article A1 (aucune catégorie n'est actuellement chargée)
        em.remove(newarticle1);
        // toplink : l'article doit être enlevé de sa catégorie sinon le test6 plante
        // hibernate : ce n'est pas nécessaire
        newarticle1.getCategorie().getArticles().remove(newarticle1);
        // fin transaction
        tx.commit();
        // dump des articles
        dumpArticles();
}
  • O Teste 4 elimina o item A1
  • linha 5: começamos com um novo contexto vazio
  • linha 10: o artigo A1 é adicionado ao contexto de persistência. Será referenciado nesse contexto por newarticle1.
  • linha 12: é removido do contexto
  • linha 15: as categorias A, B e C, e os itens A1, A2 e B1, se já não forem persistentes, continuam, no entanto, na memória. Estão simplesmente separados do contexto de persistência. O artigo A1, , que faz parte dos artigos da categoria A, é removido desta. Isto permitirá, mais tarde, voltar a associar a categoria A ao contexto de persistência. Se isto não for feito, a categoria A será reassociada com um conjunto de artigos, um dos quais foi eliminado. Isto não parece incomodar o Hibernate, mas faz com que o TopLink entre em falha.
  • Linha 19: Exibimos todos os itens para verificar se A1 desapareceu.

Os resultados são os seguintes:

1
2
3
4
main : ----------- test4
[articles]
Article[2,0,A2,1]
Article[3,0,B1,2]

O item A1 desapareceu de facto.

2.4.6.3. Teste 5

Este teste é o seguinte:


// modification d'1 article
    public static void test5() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // modification articleA2
        articleA2.setNom(articleA2.getNom() + "-");
        // articleA2 est remis dans le contexte de persistance
        em.merge(articleA2);
        // fin transaction
        tx.commit();
        // dump des articles
        dumpArticles();
    }
  • O Teste 5 altera o nome do item A2
  • Linha 4: Começamos com um novo contexto vazio
  • linha 9: alteramos o nome do item destacado A2, que passa a ser «A2-».
  • linha 11: o item desligado A2 é novamente ligado ao contexto de persistência. Note-se que A2 continua a ser um objeto desligado. É o objeto em.merge(itemA2) que agora faz parte do contexto de persistência. Este objeto não foi armazenado numa variável aqui, como é habitual. Por conseguinte, é inacessível.
  • Linha 13: Sincronização do contexto de persistência com a base de dados. O artigo A2 será modificado na base de dados e o seu número de versão mudará de N para N+1. A versão em memória destacada articleA2 já não é válida. O mesmo se aplica ao objeto destacado que representa a categoria A, porque contém o artigo A2 entre os seus artigos.
  • Linha 15: Exibimos todos os itens para verificar a alteração do nome do item A2

Os resultados são os seguintes:

1
2
3
4
main : ----------- test5
[articles]
Article[2,1,A2-,1]
Article[3,0,B1,2]

O nome do item A2 mudou, de facto.

2.4.6.4. Teste 6

Este teste é o seguinte:


// modification d'1 catégorie et de ses articles
    public static void test6() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // chargement catégorie
        categorieA = em.find(Categorie.class, categorieA.getId());
        // liste des articles de la catégorie A
        for (Article a : categorieA.getArticles()) {
            a.setNom(a.getNom() + "-");
        }
        // modification nom catégorie
        categorieA.setNom(categorieA.getNom() + "-");
        // fin transaction
        tx.commit();
        // dump des catégories et des articles
        dumpCategories();
        dumpArticles();
}
  • O Teste 6 altera o nome da categoria A e de todos os seus artigos
  • linha 4: começamos com um novo contexto vazio
  • linha 9: recuperamos a categoria A da base de dados. Não fundimos o objeto categoryA destacado porque sabemos que ele tem uma referência ao artigo A2, que se tornou obsoleto. Por isso, começamos do zero.
  • Linhas 11–12: Alteramos o nome de todos os artigos da categoria A. Mais uma vez, utilizamos a relação inversa OneToMany através do método getArticles.
  • Linha 15: O nome da categoria também é alterado
  • Linha 17: Fim da transação. O contexto é sincronizado com a base de dados. Todos os objetos no contexto que foram modificados serão atualizados na base de dados.
  • Linhas 21–22: Os itens e as categorias são apresentados para verificação

Os resultados são os seguintes:

1
2
3
4
5
6
7
8
main : ----------- test6
[categories]
Categorie[1,2,A-]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[2,2,A2--,1]
Article[3,0,B1,2]

O artigo A2 mudou de nome novamente, tal como a categoria A.

2.4.6.5. Teste 7

Este teste é o seguinte:


// category deletion
    public static void test7() {
        // new persistence context
        EntityManager em = getNewEntityManager();
        // transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistence catégorieB and cascade (merge) associated items
        Categorie mergedcategorieB = em.merge(categorieB);
        // category deletion and cascading (delete) of associated items
        em.remove(mergedcategorieB);
        // end transaction
        tx.commit();
        // dump categories and articles
        dumpCategories();
        dumpArticles();
    }
  • O Teste 7 elimina a categoria B e, consequentemente, os seus artigos
  • linha 4: começamos com um novo contexto vazio
  • linha 9: a categoria B existe na memória como um objeto separado do contexto de persistência. Nós a mesclamos de volta ao contexto de persistência. Como resultado, os seus artigos (artigo B1) também serão mesclados e, assim, reintegrados no contexto de persistência.
  • linha 11: agora que a categoria B está no contexto, podemos removê-la. Por efeito em cascata, os seus itens também serão removidos. Esta operação é possível porque a operação de fusão na linha 9 os reintegrou no contexto de persistência.
  • Linha 13: Fim da transação. O contexto será sincronizado. Os objetos no contexto que foram removidos serão eliminados da base de dados.
  • Linhas 15–16: Exibimos os itens e as categorias para verificação

Os resultados são os seguintes:

1
2
3
4
5
6
main : ----------- test7
[categories]
Categorie[1,2,A-]
Categorie[3,0,C]
[articles]
Article[1,2,A2--,1]

A categoria B e o artigo B1 desapareceram de facto.

2.4.6.6. Teste 8

Este teste é o seguinte:


// requêtes
    @SuppressWarnings("unchecked")
    public static void test8() {
        // nouveau contexte de persistance
        EntityManager em = getNewEntityManager();
        // transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // liste des articles de la catégorie A
        List articles = em
                .createQuery(
                        "select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
                .getResultList();
        // affichages articles
        System.out.println("Articles de la catégorie A");
        for (Object a : articles) {
            System.out.println(a);
        }
        // fin transaction
        tx.commit();
    }
  • O Teste 7 mostra como recuperar itens de uma categoria sem utilizar a relação inversa. Isto demonstra que a relação inversa não é essencial.
  • linha 4: começamos com um contexto novo e vazio
  • linha 10: uma consulta JPQL que recupera todos os artigos de uma categoria cujo nome começa por A
  • Linhas 15–17: Apresentação dos resultados da consulta.

Os resultados são os seguintes:

1
2
3
main : ----------- test8
Articles de la catégorie A
Article[2,2,A2--,1]

2.4.7. Projeto Eclipse / Hibernate 2

Copiamos e colamos o projeto Eclipse / Hibernate para esclarecer um ponto relativo ao conceito de relação primária / relação inversa que estabelecemos em torno da anotação @ManyToOne (primária) da @Entity [Artigo] e da relação inversa @OneToMany (inversa) da @Entity [Categoria]. Queremos mostrar que, se esta última relação não for declarada como inversa da outra, o esquema gerado para a base de dados será completamente diferente do gerado anteriormente.

Em [1] encontra-se o novo projeto Eclipse. Em [2] está o código Java e em [3] está o script Ant que irá gerar o esquema SQL da base de dados. O projeto está localizado [4] na pasta de exemplos [5]. Vamos importá-lo.

Modificamos apenas a @Entity [Category] para que a sua relação @OneToMany com a @Entity [Article] deixe de ser declarada como o inverso da relação @ManyToOne que a @Entity [Article] tem com a @Entity [Category]:


...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
 
    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @SuppressWarnings("unused")
    @Version
    private int version;
 
    @Column(length = 30)
    private String nom;
 
    // relationship OneToMany not inverse (no mappedby) Category (one) -> Article (many)
    // implemented by a Categorie_Article join table, so that, starting from a category
    // you can reach the items in this category
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();
 
    // manufacturers
...
  • linhas 18–22: continuamos a querer manter a capacidade de encontrar artigos numa determinada categoria utilizando a relação @OneToMany na linha 21. No entanto, queremos compreender o efeito do atributo mappedBy, que transforma uma relação no inverso de uma relação primária definida noutro local, noutro @Entity. Aqui, o mappedBy foi removido.

Executamos a tarefa ant-DLL (ver secção 2.1.7) com o SGBD MySQL5. O esquema resultante é o seguinte:

Observe os seguintes pontos:

  • Foi criada uma nova tabela [categorie_article] [1]. Esta não existia anteriormente.
  • Esta é uma tabela de junção entre as tabelas [categorie] [2] e [article] [3]. Se os objetos Article a1 e a2 pertencerem à categoria c1, a tabela de junção conterá as seguintes linhas:
[c1,a1]
[c1,a2]

onde c1, a1 e a2 são as chaves primárias dos objetos correspondentes.

  • A tabela de ligação [category_article] [1] foi criada pelo Hibernate para que, a partir de um objeto Category c, possamos recuperar os objetos Article a pertencentes a c. Foi a relação @OneToMany que obrigou à criação desta tabela. Como não a declarámos como o inverso da relação primária @ManyToOne da @Entity Article, o Hibernate não sabia que poderia usar esta relação primária para recuperar os artigos de uma categoria c. Por isso, encontrou outra forma de o fazer.
  • Este exemplo ajuda a esclarecer os conceitos de relações primárias e inversas. Uma (a inversa) utiliza as propriedades da outra (a primária).

O esquema SQL para esta base de dados no MySQL 5 é o seguinte:


    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D26D17756;
 
    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D424C61C9;

    alter table jpa06_hb_article 
        drop 
        foreign key FK4547168FECCE8750;
 
    drop table if exists jpa05_hb_categorie;
 
    drop table if exists jpa05_hb_categorie_jpa06_hb_article;
 
    drop table if exists jpa06_hb_article;
 
    create table jpa05_hb_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa05_hb_categorie_jpa06_hb_article (
        jpa05_hb_categorie_id bigint not null,
        articles_id bigint not null,
        primary key (jpa05_hb_categorie_id, articles_id),
        unique (articles_id)
    ) ENGINE=InnoDB;
 
    create table jpa06_hb_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;
 
    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id), 
        add constraint FK79D4BA1D26D17756 
        foreign key (jpa05_hb_categorie_id) 
        references jpa05_hb_categorie (id);
 
    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D424C61C9 (articles_id), 
        add constraint FK79D4BA1D424C61C9 
        foreign key (articles_id) 
        references jpa06_hb_article (id);
 
    alter table jpa06_hb_article 
        add index FK4547168FECCE8750 (categorie_id), 
        add constraint FK4547168FECCE8750 
        foreign key (categorie_id) 
references jpa05_hb_categorie (id);
  • Linhas 19–24: criação da tabela [categorie] e linhas 33–39: criação da tabela [article]. Note-se que estas são idênticas às do exemplo anterior.
  • Linhas 26–31: criação da tabela de junção [categorie_article] devido à presença da relação @OneToMany não inversa da @Entity Categorie. As linhas desta tabela são do tipo [c,a], em que c é a chave primária de uma categoria c e a é a chave primária de um item a pertencente à categoria c. A chave primária desta tabela de junção consiste nas duas chaves primárias [c,a] concatenadas (linha 29).
  • linhas 41-45: a restrição de chave estrangeira da tabela [categorie_article] para a tabela [categorie]
  • linhas 47–51: a restrição de chave estrangeira da tabela [categorie_article] para a tabela [article]
  • Linhas 53–57: A restrição de chave estrangeira da tabela [article] para a tabela [categorie]

O leitor é convidado a executar os testes [InitDB] e [Main]. Eles produzem os mesmos resultados de antes. No entanto, o esquema da base de dados é redundante e o desempenho será prejudicado em comparação com a versão anterior. Provavelmente devemos explorar mais a fundo esta questão das relações inversas/primárias para verificar se a nova configuração também introduz conflitos devido ao facto de termos duas relações independentes que representam a mesma coisa: a relação muitos-para-um entre a tabela [article] e a tabela [category].

Estamos agora a utilizar uma implementação JPA / Toplink:

O projeto Eclipse com Toplink é uma cópia do projeto Eclipse com Hibernate, versão 1:

O código Java é idêntico ao do projeto Hibernate anterior — versão 1. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf e ddl – script Ant) é o discutido na secção 2.1.15.2. O projeto Eclipse está disponível [3] na pasta de exemplos [4]. Vamos importá-lo.

O ficheiro <persistence.xml> [2] foi modificado num aspeto, nomeadamente as entidades declaradas:


        ...
        <!-- classes persistantes -->
        <class>entites.Categorie</class>
        <class>entites.Article</class>
...
  • Linhas 3 e 4: as duas entidades geridas

A execução de [InitDB] com o SGBD MySQL5 produz os seguintes resultados:

Em [1], a saída da consola; em [2], as duas tabelas [jpa05_tl] geradas; em [3], os scripts SQL gerados. O seu conteúdo é o seguinte:

create.sql


CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

A execução de [Main] é concluída sem erros.

Este projeto Eclipse foi criado através da clonagem do anterior. Uma vez que foi construído com o Hibernate, removemos o atributo mappedBy da relação @OneToMany da @Entity Category.


@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {
 
    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Version
    private int version;
 
    @Column(length = 30)
    private String nom;
 
    // relation OneToMany not inverse (no mappedby) Category (one) ->
    // Article (many)
    // implemented by a Categorie_Article join table, so that from
    // category
    // several items can be reached
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

O esquema SQL gerado para o MySQL5 é então o seguinte:

create.sql


CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • Linha 2: a tabela de junção que implementa a relação @OneToMany não invertida anterior.

A execução de [InitDB] é concluída sem erros, mas a execução de [Main] falha no teste 7 com os seguintes registos (FINEST):

main : ----------- test7
[TopLink Finer]: 2007.06.01 01:41:48.734--ServerSession(15290002)--Thread(Thread[main,5,main])--client acquired
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Merge clone with references Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Register the existing object Article[6,1,B1]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Register the existing object Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--The remove operation has been performed on: Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--The remove operation has been performed on: Article[6,1,B1]
[TopLink Finer]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--begin unit of work commit
[TopLink Finer]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connection(6330655)--Thread(Thread[main,5,main])--begin transaction
[TopLink Finest]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Execute query DeleteObjectQuery(Article[6,1,B1])
[TopLink Fine]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connection(6330655)--Thread(Thread[main,5,main])--DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
[TopLink Warning]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Local Exception Stack: 
Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa06_tl_categorie_jpa06_tl_article`, CONSTRAINT `FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID` FOREIGN KEY (`articles_ID`) REFERENCES `jpa06_tl_article` (`ID`))
Error Code: 1451
Call: DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
  • Linha 3: a junção na categoria B
  • linha 4: o artigo dependente B1 é colocado no contexto
  • linha 5: o mesmo para a própria categoria B
  • linha 6: a remoção na categoria B
  • linha 7: remoção do item B1 (em cascata)
  • Linha 8: O código Java solicita um commit da transação
  • linha 9: uma transação é iniciada — portanto, aparentemente ainda não tinha começado.
  • linha 10: o item B1 está prestes a ser eliminado por uma operação DELETE na tabela [item]. É aqui que reside o problema. A tabela de junção [category_item] tem uma referência à linha B1 na tabela [item]. Eliminar B1 de [item] violará uma restrição de chave estrangeira.
  • Linhas 13 e seguintes: ocorre a exceção

O que podemos concluir?

  • Mais uma vez, temos um problema de portabilidade entre o Hibernate e o Toplink: o Hibernate passou neste teste
  • O TopLink tem dificuldade em lidar com situações em que duas relações são, na verdade, inversas uma à outra, com uma não declarada como a relação primária e a outra como a inversa. Isto é aceitável porque este cenário representa, na verdade, um erro de configuração. No nosso exemplo, a tabela [article] não tem qualquer relação com a tabela de junção [categorie_article]. Parece, portanto, natural que, durante uma operação na tabela [article], o Toplink não tente trabalhar com a tabela [categorie_article].

2.5. Exemplo 5: Relação muitos-para-muitos com uma tabela de junção explícita

2.5.1. O esquema da base de dados

  • em [1], a base de dados MySQL5

Já estamos familiarizados com as tabelas [person] [2] e [address] [3]. Estas foram abordadas na Secção 2.3.1. Estamos a utilizar a versão em que a morada da pessoa é armazenada numa tabela separada [address] [3]. Na tabela [person], a relação que liga uma pessoa à sua morada é implementada através de uma restrição de chave estrangeira.

Uma pessoa realiza atividades. Estas atividades são armazenadas na tabela [activity] [4]. Uma pessoa pode realizar várias atividades e uma atividade pode ser realizada por várias pessoas. Uma relação muitos-para-muitos liga, portanto, as tabelas [person] e [activity]. Esta relação é representada pela tabela de junção [person_activity] [5].

2.5.2. Os objetos @Entity que representam a base de dados

As tabelas acima serão representadas pelas seguintes @Entities:

  • a @Entity Pessoa representará a tabela [pessoa]
  • o @Entity Address representará a tabela [address]
  • o @Entity Activity representará a tabela [activity]
  • a @Entity PersonneActivite representará a tabela [personne_activite]

As relações entre estas entidades são as seguintes:

  • Uma relação um-para-um liga a entidade Pessoa à entidade Endereço: uma pessoa p tem um endereço a. A entidade Pessoa que contém a chave estrangeira será a entidade primária, e a entidade Endereço será a entidade inversa.
  • Uma relação muitos-para-muitos liga as entidades Pessoa e Atividade: uma pessoa tem múltiplas atividades, e uma atividade é praticada por múltiplas pessoas. Esta relação poderia ser implementada diretamente utilizando uma anotação @ManyToMany em cada uma das duas entidades, com uma declarada como inversa da outra. Esta solução será explorada mais adiante. Aqui, implementamos a relação muitos-para-muitos utilizando duas relações um-para-muitos:
    • uma relação um-para-muitos que liga a entidade Pessoa à entidade PessoaAtividade: uma única linha (One) na tabela [pessoa] é referenciada por várias (Many) linhas na tabela [pessoa_atividade]. A tabela [pessoa_atividade], que contém a chave estrangeira, terá a relação primária @ManyToOne, e a entidade Pessoa terá a relação inversa @OneToMany.
    • uma relação um-para-muitos que liga a entidade Activity à entidade PersonActivity: uma (One) linha na tabela [activity] é referenciada por muitas (Many) linhas na tabela [person_activity]. A tabela [person_activity], que contém a chave estrangeira, terá a relação primária @ManyToOne, e a entidade Activity terá a relação inversa @OneToMany.

A @Entity Pessoa é a seguinte:


@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {
 
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(length = 30, nullable = false)
    private String prenom;
 
    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(nullable = false)
    private boolean marie;
 
    @Column(nullable = false)
    private int nbenfants;
 
    // main relationship Person (one) -> Address (one)
    // implemented by the foreign key Person(adresse_id) -> Address
    // cascade insert Person -> insert Address
    // cascade shift Person -> shift Address
    // cascade deletion Person -> deletion Address
    // a Person must have 1 Address (nullable=false)
    // 1 Address belongs to 1 person only (unique=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;
 
    // relation Person (one) -> PersonneActivite (many)
    // inverse of existing relationship PersonneActivite (many) -> Personne (one)
    // cascade deletion Person -> supression PersonneActivite
    @OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();
 
    // manufacturers

Esta @Entity é bem conhecida. Iremos apenas comentar as relações que mantém com outras entidades:

  • linhas 30–39: uma relação um-para-um @OneToOne com a @Entity Address, implementada através de uma chave estrangeira [address_id] (linha 38) que a tabela [Person] terá na tabela [Address].
  • Linhas 41–45: uma relação um-para-muitos (@OneToMany) com a @Entity PersonneActivite. Uma pessoa (One) é referenciada por múltiplas (Many) linhas na tabela de junção [personne_activite] representada pela @Entity PersonneActivite. Estes objetos PersonneActivite serão colocados num tipo Set<PersonneActivite>, onde PersonneActivite é um tipo que iremos definir em breve.
  • Linha 44: A relação um-para-muitos definida aqui é o inverso de uma relação primária definida no campo person da @Entity PersonneActivite (palavra-chave mappedBy). Temos uma cascata Pessoa -> Atividade em eliminações: a eliminação de uma pessoa p resultará na eliminação de elementos persistentes do tipo PersonneActivite encontrados na coleção p.activites.

A @Entity Address é a seguinte:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
 
    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;
 
  • Linhas 28-29: a relação @OneToOne que é o inverso da relação @OneToOne «adresse» da entidade @Entity Person (linhas 37-38 de Person).

A @Entity Activity é a seguinte


@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {
 
    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false, unique = true)
    private String nom;
 
    // relation Activite (one) -> PersonneActivite (many)
    // inverse of existing relationship PersonneActivite (many) -> Activite (one)
    // cascade suppression Activite -> supression PersonneActivite
    @OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();
 
  • linhas 6-9: a chave primária da atividade
  • linhas 11-13: o número da versão da atividade
  • linhas 15-16: o nome da atividade
  • linhas 18-22: a relação um-para-muitos que liga a @Entity Activity à @Entity PersonActivity: uma atividade (One) é referenciada por várias (Many) linhas na tabela de junção [person_activity] representada pela @Entity PersonActivity. Estes objetos PersonneActivite serão colocados num tipo Set<PersonneActivite>.
  • Linha 22: A relação um-para-muitos definida aqui é o inverso de uma relação primária definida no campo `activity` na `@Entity PersonneActivite` (utilizando a palavra-chave `mappedBy`). Temos uma cascata Activity -> PersonActivity em eliminações: a eliminação de uma atividade da tabela [activity] irá desencadear a eliminação das entidades PersonActivity persistentes encontradas na coleção a.people da tabela de junção [person_activity].

A @Entity PersonneActivite é a seguinte:


@Entity
// join table
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {
 
    @Embeddable
    public static class Id implements Serializable {
        // composite key components
        // points to a Person
        @Column(name = "PERSONNE_ID")
        private Long personneId;
 
        // on an Activity
        @Column(name = "ACTIVITE_ID")
        private Long activiteId;
 
        // manufacturers
...
 
        // getters and setters
...
        // toString
        public String toString() {
            return String.format("[%d,%d]", getPersonneId(), getActiviteId());
        }
    }
 
    // fields of the Personne_Activite class
    // composite key
    @EmbeddedId
    private Id id = new Id();
 
    // main relationship PersonneActivite (many) -> Nobody (one)
    // implemented by the foreign key: personneId (PersonneActivite (many) -> Personne (one)
    // personneId is also part of the composite primary key
    // JPA does not need to manage this foreign key (insertable = false, updatable = false), as this is done by the application itself in its constructor
    @ManyToOne
    @JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
    private Personne personne;
 
    // main relationship PersonneActivite -> Activity
    // implemented by the foreign key: activiteId (PersonneActivite (many) -> Activite (one)
    // activiteId is also part of the composite primary key
    // JPA does not need to manage this foreign key (insertable = false, updatable = false), as this is done by the application itself in its constructor
    @ManyToOne()
    @JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
    private Activite activite;
 
    // manufacturers
    public PersonneActivite() {
 
    }
 
    public PersonneActivite(Personne p, Activite a) {
        // foreign keys are set by the application
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // two-way associations
        this.setPersonne(p);
        this.setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
    }
 
    // getters and setters
...
    // toString
    public String toString() {
        return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
    }
}

Esta classe é mais complexa do que as anteriores.

  • A tabela [person_activity] tem linhas da forma [p,a], em que p é a chave primária de uma pessoa e a é a chave primária de uma atividade. Todas as tabelas devem ter uma chave primária, e [person_activity] não é exceção. Até agora, tínhamos definido chaves primárias geradas dinamicamente pelo SGBD. Poderíamos fazer o mesmo aqui. Vamos utilizar outra técnica, na qual a própria aplicação define os valores da chave primária de uma tabela. Aqui, uma linha [p1,a1] indica que uma pessoa p1 participa na atividade a1. Esta mesma linha não pode aparecer uma segunda vez na tabela. Assim, o par (p,a) é um bom candidato a chave primária. Isto é chamado de chave primária composta.
  • Linhas 30–31: a chave primária composta. A anotação @EmbeddedId (anteriormente @Id) é análoga à notação @Embedded aplicada ao campo Address de uma pessoa. Nesse caso, significava que o campo Address era uma instância de uma classe externa, mas tinha de ser inserido na mesma tabela que a pessoa. Aqui, o significado é o mesmo, exceto que, para indicar que estamos a lidar com a chave primária, a anotação passa a ser @EmbeddedId.
  • Linha 31: Um objeto vazio representando a chave primária `id` é criado quando o objeto `PersonneActivite` é instanciado. A classe que representa a chave primária é definida nas linhas 7–26 como uma classe pública estática interna à classe `PersonneActivite`. O facto de ser pública e estática é exigido pelo Hibernate. Se substituirmos «public static» por «private», ocorre uma exceção, e a mensagem de erro associada indica que o Hibernate tentou executar a instrução «new PersonneActivite$Id». Portanto, a classe Id deve ser tanto estática como pública.
  • Linha 6: A classe Id da chave primária é declarada como @Embeddable. Recorde-se que a chave primária id na linha 31 foi declarada como @EmbeddedId. A classe correspondente deve, portanto, ter a anotação @Embeddable.
  • Afirmámos que a chave primária da tabela [person_activity] consiste no par (p, a), em que p é a chave primária de uma pessoa e a é a chave primária de uma atividade. Encontramos os dois elementos (p, a) da chave composta [ ] na linha 11 (personId) e na linha 15 (activityId). As colunas associadas a estes dois campos são denominadas: PERSON_ID para a pessoa, ACTIVITY_ID para a atividade.
  • Linha 31: A chave primária foi definida com as suas duas colunas (PERSON_ID, ACTIVITY_ID). Não existem outras colunas na tabela [person_activity]. Resta apenas definir as relações entre a @Entity PersonneActivite que estamos atualmente a descrever e as outras @Entities no esquema relacional. Estas relações refletem as restrições de chave estrangeira que a tabela [personne_activite] tem com as outras tabelas.
  • Linhas 33–39: definem a chave estrangeira da tabela [person_activity] para a tabela [person]
  • Linha 37: A relação é do tipo @ManyToOne: uma (One) linha na tabela [person] é referenciada por muitas (Many) linhas na tabela [person_activity].
  • Linha 38: Damos um nome à coluna da chave estrangeira. Utilizamos o mesmo nome que foi dado ao componente «person» da chave estrangeira (linha 10). Os atributos insertable=false, updatable=false existem para impedir que o Hibernate gere a chave estrangeira. Esta chave é, de facto, um componente de uma chave primária calculada pela aplicação, e o Hibernate não deve intervir.
  • Linhas 41–47: Definimos a chave estrangeira da tabela [person_activity] para a tabela [activity]. As explicações são as mesmas que as dadas anteriormente.
  • Linhas 54–63: Construtor para um objeto PersonActivity baseado numa pessoa p e numa atividade a. Recorde-se que, ao construir um objeto PersonActivity, a chave primária id na linha 31 apontava para um objeto Id vazio. As linhas 56–57 atribuem um valor a cada um dos campos (personId, activityId) do objeto Id. Estes valores são, respetivamente, as chaves primárias da pessoa p e da atividade a passadas como parâmetros para o construtor. A chave primária id (linha 31) tem, portanto, agora um valor.
  • Linha 59: Ao campo «pessoa» na linha 39 é atribuído o valor «p»
  • Linha 60: Ao campo «activite» na linha 47 é atribuído o valor «a»
  • Um objeto [PersonActivity] é agora criado e inicializado. Atualizamos as relações inversas entre a @Entity Pessoa (linha 61) e a @Entity Atividade (linha 62) com a @Entity PessoaAtividade que acabou de ser criada.

Concluímos a descrição das entidades da base de dados. Encontramo-nos numa situação complexa, mas infelizmente comum. Veremos que existe outra configuração possível da camada JPA que oculta parte desta complexidade: a tabela de junção torna-se implícita, construída e gerida pela camada JPA. Aqui, escolhemos a solução mais complexa, mas que permite que o esquema relacional evolua. Isto permite que sejam adicionadas colunas à tabela de junção, o que não é possível na configuração em que a tabela de junção não é uma @Entity explícita. [ref1] recomenda a solução que estamos atualmente a examinar. A informação que permitiu o desenvolvimento desta solução foi encontrada em [ref1].

2.5.3. O Projeto Eclipse / Hibernate

A implementação JPA utilizada aqui é o Hibernate. O projeto Eclipse para os testes é o seguinte:

 

Image

Em [1], o projeto Eclipse; em [2], o código Java. O projeto está localizado em [3], dentro da pasta de exemplos [4]. Vamos importá-lo.

2.5.4. Geração do DDL da base de dados

Seguindo as instruções da secção 2.1.7, o DDL gerado para o SGBD MySQL5 é o seguinte:


alter table jpa07_hb_personne 
        drop 
        foreign key FKB5C817D45FE379D0;
 
    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B06CD852024;
 
    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B0668C7A284;
 
    drop table if exists jpa07_hb_activite;
 
    drop table if exists jpa07_hb_adresse;
 
    drop table if exists jpa07_hb_personne;
 
    drop table if exists jpa07_hb_personne_activite;
 
    create table jpa07_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa07_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa07_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa07_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;
 
    alter table jpa07_hb_personne 
        add index FKB5C817D45FE379D0 (adresse_id), 
        add constraint FKB5C817D45FE379D0 
        foreign key (adresse_id) 
        references jpa07_hb_adresse (id);
 
    alter table jpa07_hb_personne_activite 
        add index FKD3E49B06CD852024 (ACTIVITE_ID), 
        add constraint FKD3E49B06CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa07_hb_activite (id);
 
    alter table jpa07_hb_personne_activite 
        add index FKD3E49B0668C7A284 (PERSONNE_ID), 
        add constraint FKD3E49B0668C7A284 
        foreign key (PERSONNE_ID) 
        references jpa07_hb_personne (id);
  • linhas 21-26: a tabela [activity]
  • linhas 28-39: a tabela [endereço]
  • linhas 41-51: a tabela [person]
  • linhas 53-57: a tabela de junção [pessoa_atividade]. Observe a chave composta (linha 56)
  • linhas 59-63: a chave estrangeira da tabela [pessoa] para a tabela [endereço]
  • linhas 65-69: a chave estrangeira da tabela [pessoa_atividade] para a tabela [atividade]
  • linhas 71-75: a chave estrangeira da tabela [pessoa_atividade] para a tabela [pessoa]

2.5.5. InitDB

O código para [InitDB] é o seguinte:


package tests;
 
...
public class InitDB {
 
    // constant
    private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";
 
    private final static String TABLE_PERSONNE = "jpa07_hb_personne";
 
    private final static String TABLE_ACTIVITE = "jpa07_hb_activite";
 
    private final static String TABLE_ADRESSE = "jpa07_hb_adresse";
 
    public static void main(String[] args) throws ParseException {
        // Persistence context
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // we retrieve a EntityManager from the EntityManagerFactory
        // previous
        em = emf.createEntityManager();
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // request
        Query sql1;
        // delete elements from the PERSONNE_ACTIVITE table
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
        sql1.executeUpdate();
        // delete elements from the PERSONNE table
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // delete elements from the ACTIVITE table
        sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
        sql1.executeUpdate();
        // delete elements from the ADRESSE table
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // creation activities
        Activite act1 = new Activite();
        act1.setNom("act1");
        Activite act2 = new Activite();
        act2.setNom("act2");
        Activite act3 = new Activite();
        act3.setNom("act3");
        // persistence activities
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // creating people
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // address creation
        Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
        Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
        Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
        // associations person <--> address
        p1.setAdresse(adr1);
        adr1.setPersonne(p1);
        p2.setAdresse(adr2);
        adr2.setPersonne(p2);
        p3.setAdresse(adr3);
        adr3.setPersonne(p3);
        // persistence of persons and therefore of associated addresses
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // persistence of a4 address not linked to a person
        em.persist(adr4);
        // people display
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // address display
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        // associations person <-->activity
        PersonneActivite p1act1 = new PersonneActivite(p1, act1);
        PersonneActivite p1act2 = new PersonneActivite(p1, act2);
        PersonneActivite p2act1 = new PersonneActivite(p2, act1);
        PersonneActivite p2act3 = new PersonneActivite(p2, act3);
        // persistence of person <--> activity associations
        em.persist(p1act1);
        em.persist(p1act2);
        em.persist(p2act1);
        em.persist(p2act3);
        // people display
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // address display
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[personnes/activites]");
        for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
            System.out.println(pa);
        }
        // end transaction
        tx.commit();
        // end EntityManager
        em.close();
        // end EntityManagerFactory
        emf.close();
        // log
        System.out.println("terminé...");
 
    }
}
  • linhas 27-38: as tabelas [person_activity], [person], [address] e [activity] são esvaziadas. Note que devemos começar pelas tabelas que possuem chaves estrangeiras.
  • linhas 40-45: criamos três atividades: act1, act2 e act3
  • linhas 47–49: são colocadas no contexto de persistência.
  • linhas 51-53: são criadas três pessoas, p1, p2 e p3.
  • Linhas 55–58: são criados quatro endereços (adr1 a adr4).
  • Linhas 60–65: Os endereços adr1–adr4 são associados às pessoas p1–p3. Há duas operações a realizar de cada vez, porque a relação Pessoa <-> Endereço é bidirecional.
  • linhas 67–69: as pessoas p1 a p3 são colocadas no contexto de persistência. Devido à cascata Pessoa -> Endereço, o mesmo acontecerá com os endereços adr1 a adr3.
  • Linha 71: O quarto endereço, adr4, que não está associado a uma pessoa, é explicitamente colocado no contexto de persistência.
  • Linhas 73–85: O contexto de persistência é consultado para recuperar as listas de entidades dos tipos [Pessoa], [Endereço] e [Atividade]. Sabemos que estas consultas irão desencadear a sincronização do contexto com a base de dados: as entidades criadas serão inseridas na base de dados e receberão as suas chaves primárias. É importante compreender isto para o que se segue.
  • Linhas 87–90: Criamos quatro associações Pessoa <-> Atividade. Os seus nomes indicam qual a pessoa que está ligada a qual atividade. Poderá recordar-se de que a chave primária de uma entidade PessoaAtividade é uma chave composta que consiste nas chaves primárias de uma Pessoa e de uma Atividade. Esta operação é possível porque as entidades Pessoa e Atividade obtiveram as suas chaves primárias durante uma sincronização anterior.
  • Linhas 92–95: Estas 4 associações são adicionadas ao contexto de persistência.
  • Linhas 87–86: O contexto de persistência é consultado para recuperar as listas de entidades dos tipos [Pessoa], [Endereço], [Atividade] e [PessoaAtividade]. Sabemos que estas consultas irão desencadear a sincronização do contexto com a base de dados: as entidades PessoaAtividade criadas serão inseridas na base de dados.

A execução de [InitDB] com o MySQL5 produz a seguinte saída na consola:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
terminé...

Pode ser surpreendente ver que, nas linhas 15–16, os números de versão para as pessoas p1 e p2 são 1, e que o mesmo se verifica nas linhas 24–26 para as três atividades. Vamos tentar compreender.

Nas linhas 2–4, os números de versão para as pessoas são 0, e nas linhas 11–13, os números de versão para as atividades são 0. Estas exibições ocorrem antes de as relações Pessoa <-> Atividade serem criadas. As linhas 87–90 do código Java criam relações entre as pessoas p1 e p2 e as atividades act1, act2 e act3. Estas são criadas utilizando o construtor @Entity PersonneActivite (ver Secção 2.5.2). A leitura do código deste construtor mostra que, quando uma pessoa p é ligada a uma atividade a:

  • a atividade a é adicionada ao conjunto p.activities
  • a pessoa p é adicionada ao conjunto a.personnes

Assim, quando escrevemos new PersonneActivite(p, a)*, a pessoa p e a atividade a sofrem uma modificação na memória. Quando as linhas 97–113 de [InitDB] são executadas, o contexto de persistência é sincronizado com a base de dados, e o JPA/Hibernate deteta que as entidades persistentes p1, p2, act1, act2 e act3* foram modificadas. Estas alterações devem ser feitas na base de dados. Na verdade, são gravadas na tabela de junção [person_activity], mas o JPA/Hibernate continua a incrementar o número de versão de cada entidade persistente modificada.

Na vista do SQL Explorer, os resultados são os seguintes:

  • [2]: a tabela [jpa07_hb_*]
  • [3]: a tabela people
  • [4]: a tabela de endereços.
  • [5]: a tabela de atividades
  • [6]: a tabela de junção pessoa <-> atividade

2.5.6. Main

A classe [Main] executa uma série de testes pelos quais passamos, exceto o teste 1, que utiliza o código de [InitDB] para inicializar a base de dados.

2.5.6.1. Teste 2

Este teste é o seguinte:


// suppression Personne p1
    public static void test2() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // suppression dépendances sur p1 : pas nécessaire à hibernate mais
        // indispensable à toplink
        act1.getPersonnes().remove(p1act1);
        act2.getPersonnes().remove(p1act2);
        // suppression personne p1
        em.remove(p1);
        // fin transaction
        tx.commit();
        // on affiche les nouvelles tables
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • Linha 4: Utilizamos o contexto de persistência de test1, onde a pessoa p1 é um objeto nesse contexto.
  • linha 13: eliminação da pessoa p1. Devido ao atributo:
    • cascadeType.ALL em Endereço, o endereço associado à pessoa p1 será eliminado
    • cascadeType.REMOVE em PersonActivity, as atividades da pessoa p1 serão eliminadas.
  • Linhas 10–11: Removemos as dependências que outras entidades têm da pessoa p1, que será eliminada na linha 13. As atividades act1 e act2 são realizadas pela pessoa p1. As ligações foram criadas pelo construtor da entidade PersonActivity, cujo código é o seguinte:

    public PersonneActivite(Personne p, Activite a) {
        // les clés étrangères sont fixées par l'application
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // associations bidirectionnelles
        setPersonne(p);
        setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
}

Na linha 9, a atividade a recebe um elemento adicional do tipo PersonActivity na sua coleção de pessoas. Este elemento é do tipo (p,a) para indicar que a pessoa p participa na atividade a. No test1 dentro de [Main], foram criados dois links (p1,act1) e (p1,act2) desta forma. As linhas 10 e 11 do test2 removem estas dependências. Note-se que o Hibernate funciona sem remover estas dependências na pessoa p1, mas o Toplink não.

  • Linhas 17–20: todas as tabelas são exibidas

Os resultados são os seguintes:

main : ----------- test1
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
  • A pessoa p1, que está presente no teste1 (linha 3), já não está presente no final do teste2 (linhas 22–23)
  • O endereço adr1 da pessoa p1, presente no teste1 (linha 11), já não está presente após o teste2 (linhas 29–31)
  • as atividades (p1,act1) (linha 16) e (p1,act2) (linha 18) da pessoa p1, presentes no teste1, já não estão presentes no final do teste2 (linhas 33-34)

2.5.6.2. Teste3

Este teste é o seguinte:


// suppression activite act1
    public static void test3() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // suppression dépendances sur act1 : pas nécessaire à hibernate mais
        // indispensable à toplink
        p2.getActivites().remove(p2act1);
        // suppression activité act1
        em.remove(act1);
        // fin transaction
        tx.commit();
        // on affiche les nouvelles tables
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • Linha 4: Utilizamos o contexto de persistência de test2
  • linha 12: eliminação da atividade act1. Devido ao atributo:
    • cascadeType.REMOVE em PersonneActivite, as linhas (p, act1) na tabela [personne_activite] serão eliminadas.
  • Linha 10: Antes de remover act1 do contexto de persistência, removemos quaisquer dependências que outras entidades possam ter sobre este objeto persistente. Após a eliminação da pessoa p1 no teste anterior, apenas a pessoa p2 realiza a atividade act1.
  • Linhas 13–16: Todas as tabelas são apresentadas

Os resultados são os seguintes:

main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,3],p2,act3]
  • No teste2, a atividade act1 existe (linha 6). No teste3, já não existe (linhas 21-22)
  • No teste2, o link (p2,act1) existe (linha 14). No teste3, já não existe (linha 28)

2.5.6.3. Teste4

Este teste é o seguinte:


// récupération activités d'une personne
    public static void test4() {
        // contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on récupère la personne p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
        // on scanne ses activités
        for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
            System.out.println(pa);
        }
        // on passe par la relation inverse de p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
        // on scanne ses activités
        for (PersonneActivite pa : p2.getActivites()) {
            System.out.println(pa.getActivite().getNom());
        }
        // fin transaction
        tx.commit();
    }
  • O Teste 4 apresenta as atividades da pessoa p2.
  • linha 4: começamos com um novo contexto vazio
  • linhas 12–14: exibimos os nomes das atividades realizadas pela pessoa p2 utilizando uma consulta JPQL.
    • É realizada uma junção entre Activity (a) e PersonActivity (pa) (join a.people)
    • Nas linhas desta junção (a, pa), exibimos o nome da atividade (a.name) para a pessoa p2 (pa.person.name='p2').
  • Linhas 16–21: Fazemos o mesmo que antes, mas utilizando a relação OneToMany p2.activities da pessoa p2. A consulta JPQL será gerada pelo JPA. Aqui vemos a vantagem da relação OneToMany inversa: evita uma consulta JPQL.

Os resultados são os seguintes:

1
2
3
4
5
main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation inverse) :
act3

2.5.6.4. Teste 5

Este teste é o seguinte:


// récupération personnes faisant une activité donnée
    public static void test5() {
        // contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // on demande les activités de p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // on passe par la relation inverse de act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (PersonneActivite pa : act3.getPersonnes()) {
            System.out.println(pa.getPersonne().getNom());
        }
        // fin transaction
        tx.commit();
    }
  • O Teste 6 mostra as pessoas a realizar a atividade act3. A abordagem é semelhante à do Teste 6. Deixamos ao leitor a tarefa de estabelecer a ligação entre os dois trechos de código.

Os resultados são os seguintes:

1
2
3
4
5
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2

Os testes 4 e 5 tinham como objetivo demonstrar, mais uma vez, que uma relação inversa nunca é essencial e pode sempre ser substituída por uma consulta JPQL.

Estamos agora a utilizar uma implementação JPA / Toplink:

O projeto Eclipse com Toplink é uma cópia do projeto Eclipse com Hibernate:

O código Java é idêntico ao do projeto Hibernate anterior, com algumas pequenas diferenças que iremos discutir. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf e ddl – script Ant) é o descrito na secção 2.1.15.2. O projeto Eclipse está disponível [3] na pasta de exemplos [4]. Iremos importá-lo.

O ficheiro <persistence.xml> [2] foi modificado num aspeto: as entidades declaradas:


        <!-- classes persistantes -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
<class>entites.PersonneActivite</class>
  • Linhas 2–5: as quatro entidades geridas

A execução de [InitDB] com o SGBD MySQL5 produz os seguintes resultados:

Em [1], a saída da consola; em [2], as tabelas [jpa07_tl] geradas; em [3], os scripts SQL gerados. Os seus conteúdos são os seguintes:

create.sql


CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

A execução de [InitDB] e [Main] é concluída sem erros.

2.6. Exemplo 6: Relação muitos-para-muitos com uma tabela de junção implícita

Voltamos ao Exemplo 4, mas agora tratamos o caso com uma tabela de junção implícita gerada pela própria camada JPA.

2.6.1. O esquema da base de dados

  • em [1], a base de dados MySQL5 – em [2]: a tabela [person] – em [3]: a tabela associada [address] – em [4]: a tabela [activity] para atividades – em [5]: a tabela de junção [person_activity] que liga pessoas e atividades.

2.6.2. Os objetos @Entity que representam a base de dados

As tabelas acima serão representadas pelas seguintes anotações @Entity:

  • O @Entity Person representará a tabela [person]
  • a @Entity Address representará a tabela [address]
  • o @Entity Activity representará a tabela [activity]
  • A tabela [person_activity] já não é representada por uma @Entity

As relações entre estas entidades são as seguintes:

  • Uma relação um-para-um liga a entidade Pessoa à entidade Endereço: uma pessoa p tem um endereço a. A entidade Pessoa que contém a chave estrangeira será a entidade primária, e a entidade Endereço será a entidade inversa.
  • Uma relação muitos-para-muitos liga as entidades Pessoa e Atividade: uma pessoa tem várias atividades e uma atividade é praticada por várias pessoas. Esta relação será implementada utilizando uma anotação @ManyToMany em cada uma das duas entidades, com uma declarada como inversa da outra.

A @Entity Pessoa é a seguinte:


@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {
 
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver :@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false, unique = true)
    private String nom;
 
    @Column(length = 30, nullable = false)
    private String prenom;
 
    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;
 
    @Column(nullable = false)
    private boolean marie;
 
    @Column(nullable = false)
    private int nbenfants;
 
    // main relationship Person (one) -> Address (one)
    // implemented by the foreign key Person(adresse_id) -> Address
    // cascade insert Person -> insert Address
    // cascade shift Person -> shift Address
    // cascade deletion Person -> deletion Address
    // a Person must have 1 Address (nullable=false)
    // 1 Address belongs to 1 person only (unique=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relationship Person (many) -> Activity (many) via a personne_activite join table
    // personne_activite(PERSONNE_ID) is a foreign key on Person(id)
    // personne_activite(ACTIVITE_ID) is a foreign key on Activite(id)
    // cascade=CascadeType.PERSIST: persistence of 1 person leads to persistence of their activities
    @ManyToMany(cascade={CascadeType.PERSIST})
    @JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
    private Set<Activite> activites = new HashSet<Activite>();
 
    // manufacturers
    public Personne() {
    }
 

Iremos comentar apenas a relação @ManyToMany nas linhas 46–48, que liga a @Entity Pessoa à @Entity Atividade:

  • linha 48: uma pessoa tem atividades. O campo activities irá representá-las. Na versão anterior, o tipo dos elementos no conjunto activities era PersonActivity. Aqui, é Activity. Assim, acedemos diretamente às atividades de uma pessoa, enquanto que na versão anterior tínhamos de passar pela entidade intermediária PersonActivity.
  • Linha 46: A relação que liga a @Entity Pessoa que estamos a examinar à @Entity Atividade no conjunto de atividades na linha 48 é do tipo muitos-para-muitos (ManyToMany):
    • uma pessoa (One) tem múltiplas atividades (Many)
    • uma atividade (One) é praticada por várias pessoas (Many)
    • Em última análise, as @Entity Person e Activity estão ligadas por uma relação ManyToMany. Tal como na relação OneToOne, as entidades nesta relação são simétricas. Podemos escolher livremente qual das @Entity irá manter a relação primária e qual irá manter a relação inversa. Aqui, decidimos que a @Entity Person irá manter a relação primária.
    • Tal como vimos no exemplo anterior, a relação @ManyToMany requer uma tabela de junção. Enquanto anteriormente definimos isto utilizando uma @Entity, a tabela de junção aqui é definida utilizando a anotação @JoinTable na linha 47.
      • O atributo name atribui um nome à tabela.
      • A tabela de junção consiste nas chaves estrangeiras das tabelas que ela une. Aqui, existem duas chaves estrangeiras: uma da tabela [person] e outra da tabela [activity]. Estas colunas de chave estrangeira são definidas pelos atributos joinColumns e inverseJoinColumns.
      • A anotação @JoinColumn no atributo joinColumns define a chave estrangeira na tabela da @Entity que mantém a relação @ManyToMany primária, neste caso a tabela [person]. Esta coluna de chave estrangeira será denominada PERSON_ID.
      • A anotação @JoinColumn do atributo inverseJoinColumns define a chave estrangeira na tabela da @Entity que mantém a relação @ManyToMany inversa, neste caso a tabela [activity]. Esta coluna de chave estrangeira será denominada ACTIVITY_ID.

A @Entity Address é a seguinte:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;
 
  • Linhas 28-29: a relação @OneToOne que é o inverso da relação @OneToOne «adresse» da entidade @Entity Person (linhas 37-38 de Person).

A @Entity Activity é a seguinte


@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {
 
    // fields
    @Id()
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver : @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false)
    @Version
    private int version;
 
    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // inverse relationship Activity -> Person
    @ManyToMany(mappedBy = "activites")
    private Set<Personne> personnes = new HashSet<Personne>();
...
  • Linhas 20–21: A relação muitos-para-muitos que liga a @Entity Activity à @Entity Person. Esta relação já foi definida na @Entity Person. Aqui, especificamos simplesmente que a relação é o inverso (mappedBy) da relação @ManyToMany existente no campo activites (mappedBy="activites") da @Entity Person.
  • Lembre-se de que uma relação inversa é sempre opcional. Aqui, utilizamo-la para recuperar as pessoas que participam na atividade atual. A coleção Set<Pessoa> pessoas será utilizada para as recuperar. O modo de carregamento para as dependências Pessoa da @Entity Atividade não está especificado. Também não o especificámos no exemplo anterior. Por predefinição, este modo é fetch=FetchType.LAZY.

Concluímos a descrição das entidades da base de dados. Isto foi mais simples do que no caso em que a tabela de junção [person_activity] é uma tabela explícita. Esta solução mais simples pode apresentar desvantagens ao longo do tempo: não permite adicionar colunas à tabela de junção. No entanto, isto pode revelar-se necessário para satisfazer novos requisitos, tais como adicionar uma coluna à tabela [person_activity] indicando a data em que a pessoa se inscreveu na atividade.

2.6.3. O Projeto Eclipse / Hibernate

A implementação JPA utilizada aqui é o Hibernate. O projeto Eclipse para os testes é o seguinte:

Em [1], o projeto Eclipse; em [2], o código Java. O projeto está localizado em [3], dentro da pasta de exemplos [4]. Vamos importá-lo.

2.6.4. Geração do DDL da base de dados

Seguindo as instruções da secção 2.1.7, o DDL gerado para o SGBD MySQL5 é o seguinte:


alter table jpa08_hb_personne 
        drop 
        foreign key FKA44B1E555FE379D0;
 
    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A5CD852024;
 
    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A568C7A284;
 
    drop table if exists jpa08_hb_activite;
 
    drop table if exists jpa08_hb_adresse;
 
    drop table if exists jpa08_hb_personne;
 
    drop table if exists jpa08_hb_personne_activite;
 
    create table jpa08_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa08_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa08_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa08_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;
 
    alter table jpa08_hb_personne 
        add index FKA44B1E555FE379D0 (adresse_id), 
        add constraint FKA44B1E555FE379D0 
        foreign key (adresse_id) 
        references jpa08_hb_adresse (id);
 
    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A5CD852024 (ACTIVITE_ID), 
        add constraint FK5A6A55A5CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa08_hb_activite (id);
 
    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A568C7A284 (PERSONNE_ID), 
        add constraint FK5A6A55A568C7A284 
        foreign key (PERSONNE_ID) 
        references jpa08_hb_personne (id);

Este DDL é análogo ao obtido com a tabela de junção explícita e corresponde ao esquema já apresentado:

2.6.5. InitDB

Não vamos comentar muito sobre a classe [InitDB], que é idêntica à sua versão anterior e produz os mesmos resultados. Em vez disso, vamos concentrar-nos no código seguinte, que apresenta a junção Pessoa <-> Atividade:


        // people/activities display
        System.out.println("[personnes/activites]");
        Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
        while (iterator.hasNext()) {
            Object[] row = (Object[]) iterator.next();
            System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
  • Linha 3: A consulta JPQL que realiza a junção. O resultado da instrução SELECT devolve os IDs das entidades Pessoa e Atividade ligadas pela tabela de junção. A lista devolvida pela instrução SELECT é composta por linhas que contêm dois objetos Long. Para percorrer esta lista, a linha 3 solicita um objeto Iterator para a lista.
  • Linhas 4–7: Utilizando o objeto Iterator da linha anterior, a lista é percorrida.
    • Linha 5: Cada elemento da lista é uma matriz que contém uma linha do resultado da instrução SELECT
    • Linha 6: Os elementos da linha atual resultantes da instrução SELECT são recuperados através das conversões de tipo apropriadas.

O resultado de [InitDB] é o seguinte:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
terminé...

2.6.6. Main

A classe [Main] executa uma série de testes, alguns dos quais iremos analisar.

2.6.6.1. Teste3

Este teste é o seguinte:


// suppression activite act1
    public static void test3() {
        // contexte de persistance
        EntityManager em = getEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // suppression activité act1 de p2
        p2.getActivites().remove(act1);
        // on retire act1 du contexte de persistance
        em.remove(act1);
        // fin transactions
        tx.commit();
        // on affiche les nouvelles tables
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • Linha 11: A atividade act1 é removida do contexto de persistência
  • Linha 9: A atividade act1 é uma das atividades da única pessoa que permanece no contexto, a pessoa p2. A linha 9 remove a atividade act1 das atividades da pessoa p2. Fazemos isto para manter a consistência do contexto de persistência, uma vez que iremos utilizá-lo mais tarde.

Os resultados são os seguintes:

main : ----------- test1
[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
main : ----------- test2
[personnes]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,1]
[2,3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,3]
  • A atividade act1 na linha 26 do teste2 desapareceu das atividades do teste3 (linhas 40-41)
  • A pessoa p2 tinha a atividade act1 no teste2 (linha 33). No final do teste3, já não a tem (linha 47)

2.6.6.2. Teste6

Este teste é o seguinte:


// modification des activités d'une personne
    public static void test6() {
        // contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // on récupère la personne p2
        p2 = em.find(Personne.class, p2.getId());
        // on récupère l'activité act2
        act2 = em.find(Activite.class, act2.getId());
        // p2 ne pratique plus que l'activité act2
        p2.getActivites().clear();
        p2.getActivites().add(act2);
        // fin transaction
        tx.commit();
        // on affiche les nouvelles tables
        dumpPersonne();
        dumpActivite();
        dumpPersonne_Activite();
    }
  • Linha 4: É utilizado um novo contexto de persistência vazio
  • linha 9: a pessoa p2 é recuperada da base de dados para o contexto de persistência
  • linha 11: a atividade act2 é recuperada da base de dados para o contexto de persistência
  • linha 13: as atividades da pessoa p2 (act3) são recuperadas da base de dados para o contexto (fetchType.LAZY). A chamada [getActivites] desencadeia este carregamento. Removemos as atividades de p2. Não se trata de uma remoção efetiva das atividades (remove), mas de uma modificação do estado da pessoa p2. Esta já não participa em nenhuma atividade.
  • Linha 14: A atividade act2 é adicionada à pessoa p2. Em última análise, o conjunto de novas atividades para a pessoa p2 é o conjunto {act2}.
  • Linha 16: Fim da transação. A sincronização irá rever os objetos no contexto (p2, act2, act3) e irá detetar que o estado de p2 mudou. As instruções SQL que refletem esta alteração na base de dados serão executadas.
  • Linhas 18–20: Todas as tabelas são apresentadas

Os resultados são os seguintes:

main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation principale) :
act3
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2
main : ----------- test6
[personnes]
P[2,2,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes/activites]
[2,2]
  • No final do teste 4, a pessoa p2 estava a realizar a atividade act3 (linha 3).
  • No final do teste 6 (linha 19), a pessoa p2 já não está a realizar a atividade act3 (linha 3) e está a realizar a atividade act2.

Estamos agora a utilizar uma implementação JPA / Toplink:

O projeto Eclipse com Toplink é uma cópia do projeto Eclipse com Hibernate:

O ficheiro <persistence.xml> [2] foi modificado num ponto, especificamente no que diz respeito às entidades declaradas:


        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classes persistantes -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
...
  • linhas 4-6: as entidades geridas

A execução do [InitDB] com o SGBD MySQL5 produz os seguintes resultados:

Em [1], a saída da consola; em [2], as tabelas [jpa07_tl] geradas; em [3], os scripts SQL gerados. O seu conteúdo é o seguinte:

create.sql


CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

A execução de [InitDB] e [Main] é concluída sem erros.

2.6.8. O projeto Eclipse / Hibernate 2

Criamos um projeto Eclipse com base no anterior, copiando-o:

Em [1], o projeto Eclipse; em [2], o código Java. O projeto está localizado em [3] dentro da pasta de exemplos [4]. Vamos importá-lo.

Modificamos a relação que liga Pessoa a Atividade da seguinte forma:

Pessoa


    // relation Personne (many) -> Activite (many) via une table de jointure personne_activite
    // personne_activite(PERSONNE_ID) est clé étangère sur Personne(id)
    // personne_activite(ACTIVITE_ID) est clé étangère sur Activite(id)
    // plus de cascade sur les activités
    // @ManyToMany(cascade={CascadeType.PERSIST})
    @ManyToMany()
    @JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
  • Linha 6: A relação @ManyToMany primária já não tem uma cascata de persistência Pessoa -> Atividade (ver versão anterior, linha 5)

Activity


    // plus de relation inverse avec Personne
    // @ManyToMany(mappedBy = "activites")
// private Set<Personne> personnes = new HashSet<Personne>();
  • Linhas 2-3: A relação inversa @ManyToMany Atividade -> Pessoa foi removida

O nosso objetivo é demonstrar que os atributos removidos (cascata e relação inversa) não são essenciais. A primeira alteração introduzida por esta nova configuração encontra-se em [InitDB]:


        // associations personnes <--> activites
        p1.getActivites().add(act1);
        p1.getActivites().add(act2);
        p2.getActivites().add(act1);
        p2.getActivites().add(act3);
        // persistance des activites
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // persistance des personnes
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // et de l'adresse a4 non liée à une personne
em.persist(adr4);
  • linhas 7–9: somos obrigados a colocar explicitamente as atividades act1 a act3 no contexto de persistência. Quando existia a cascata de persistência Pessoa -> Atividade, as linhas 11–13 persistiam tanto as pessoas p1 a p3 como as atividades dessas pessoas, act1 a act3.

Uma segunda alteração é visível em [Main]:


    // récupération personnes faisant une activité donnée
    public static void test5() {
        // contexte de persistance
        EntityManager em = getNewEntityManager();
        // début transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // on demande les activités de p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // fin transaction
        tx.commit();
}
  • linhas 9-12: a consulta JPQL que recupera as pessoas que participam na atividade act3
  • Na versão anterior, o mesmo resultado também era obtido através da relação inversa Activity -> Person, que agora foi removida:

        // we use the inverse relationship of act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (Personne p : act3.getPersonnes()) {
            System.out.println(p.getNom());
}

Estamos a criar um projeto Eclipse com base no projeto Eclipse / Toplink anterior, copiando-o:

Em [1], o projeto Eclipse; em [2], o código Java. O projeto está localizado em [3] na pasta de exemplos [4]. Vamos importá-lo.

O código Java é idêntico ao da versão Hibernate.

2.7. Exemplo 7: Utilização de consultas nomeadas

Concluímos esta extensa visão geral das entidades JPA, que teve início no parágrafo 2, com um exemplo final que demonstra a utilização de consultas JPQL externalizadas num ficheiro de configuração. Este exemplo foi retirado da seguinte fonte:

[ref2]: «Getting Started With JPA in Spring 2.0», de Mark Fisher, disponível no URL

[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].

2.7.1. A base de dados de exemplo

A base de dados é a seguinte:

  • em [1]: uma lista de restaurantes com os seus nomes e endereços
  • em [2]: a tabela de endereços dos restaurantes, limitada ao número e nome da rua. Existe uma relação um-para-um entre as tabelas de restaurantes e endereços: um restaurante tem um e apenas um endereço.
  • em [3]: uma tabela de pratos com os seus nomes e um indicador verdadeiro/falso que indica se o prato é vegetariano ou não
  • em [4]: a tabela de ligação restaurante/prato: um restaurante serve vários pratos, e o mesmo prato pode ser servido por vários restaurantes. Existe uma relação muitos-para-muitos entre as tabelas de restaurantes e de pratos.

2.7.2. Os objetos @Entity que representam a base de dados

As tabelas acima serão representadas pelas seguintes @Entities:

  • a @Entity Restaurant representará a tabela [restaurant]
  • o @Entity Address representará a tabela [address]
  • o @Entity Dish representará a tabela [dish]

As relações entre estas entidades são as seguintes:

  • Uma relação um-para-um liga a entidade Restaurant à entidade Address: um restaurante r tem um endereço a. A entidade Restaurant, que contém a chave estrangeira, será a entidade primária. A entidade Address não terá uma relação inversa.
  • Uma relação muitos-para-muitos liga as entidades «Restaurante» e «Prato»: um restaurante serve vários pratos e o mesmo prato pode ser servido por vários restaurantes. Esta relação será implementada utilizando uma anotação @ManyToMany na entidade «Restaurante». A entidade «Prato» não terá uma relação inversa.

A @Entity Restaurant é a seguinte:


package entites;
 
...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {
 
    private static final long serialVersionUID = 1L;
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
 
    @Column(unique = true, length = 30, nullable = false)
    private String nom;
 
    @OneToOne(cascade = CascadeType.ALL)
    private Adresse adresse;
 
    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
    private Set<Plat> plats = new HashSet<Plat>();
 
    // manufacturers
    public Restaurant() {
 
    }
 
    public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
    }
 
    // getters and setters
...
 
    // toString
    public String toString() {
        String signature = "R[" + getNom() + "," + getAdresse();
        for (Plat e : getPlats()) {
            signature += "," + e;
        }
        return signature + "]";
    }
}
  • Linha 17: A relação um-para-um entre a entidade Restaurant e a entidade Address. Todas as operações de persistência num restaurante são propagadas para o seu endereço.
  • linha 20: a relação que liga a @Entity Restaurant à @Entity Dish na coleção de pratos na linha 22 é do tipo muitos-para-muitos (ManyToMany):
    • um restaurante (One) tem vários pratos (Many)
    • um prato (One) pode ser servido por vários restaurantes (Many)
    • Em última análise, a @Entity Restaurant e a @Entity Dish estão ligadas por uma relação ManyToMany. Decidimos que a @Entity Restaurant será a relação primária e que a @Entity Dish não terá uma relação inversa.
    • A relação @ManyToMany requer uma tabela de junção. Esta é definida utilizando a anotação @JoinTable na linha 47.
      • O atributo name atribui um nome à tabela.
      • A tabela de junção consiste nas chaves estrangeiras das tabelas que ela une. Aqui, existem duas chaves estrangeiras: uma da tabela [restaurant] e a outra da tabela [dish]. Estas colunas de chave estrangeira são definidas pelos atributos joinColumns e inverseJoinColumns.
      • O atributo joinColumns define a chave estrangeira na tabela da @Entity que mantém a relação @ManyToMany primária, neste caso a tabela [restaurant]. O atributo joinColumns está em falta aqui. O JPA tem um valor por defeito neste caso: [table]_[table_primary_key], aqui [jpa10_hb_restaurant_id].
      • A anotação @JoinColumn para o atributo inverseJoinColumns define a chave estrangeira na tabela da @Entity que mantém a relação @ManyToMany inversa, neste caso a tabela [dish]. Esta coluna de chave estrangeira será denominada dish_id.

A @Entity Address é a seguinte:


package entites;
 
...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  
  @Column(name = "NUMERO_RUE")
  private int numeroRue;
  
  @Column(name = "NOM_RUE", length=30, nullable=false)
  private String nomRue;
  
  // getters and setters
 ...
 
  // manufacturers
  public Adresse(int streetNumber, String streetName){
...
  }
  
  public Adresse(){
    
  }
  
  // toString
  public String toString(){
    return "A["+getNumeroRue()+","+getNomRue()+"]";
  }
}
  • A @Entity Address é uma entidade sem relação direta com outras entidades. Só pode ser persistida através de uma entidade Restaurant.
  • Um endereço é definido por um nome de rua (linha 16) e um número de porta (linha 13).

A @Entity Dish é a seguinte


package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
 
    @Column(unique=true, length=50, nullable=false)
    private String nom;
 
    private boolean vegetarien;
 
    // manufacturers
    public Plat() {
 
    }
 
    public Plat(String name, boolean vegetarian) {
...
    }
 
    // getters and setters
...
 
    // toString
    public String toString() {
        return "E[" + getNom() + "," + isVegetarien() + "]";
    }
 
}
  • A @Entity Dish é uma entidade sem relação direta com outras entidades. Só pode ser persistida através de uma entidade Restaurant.
  • Um prato é definido por um nome (linha 12) e se é vegetariano ou não (linha 14).

2.7.3. O projeto Eclipse / Hibernate

A implementação JPA utilizada aqui é o Hibernate. O projeto de teste do Eclipse é o seguinte:

Em [1], o projeto Eclipse; em [2], o código Java e a configuração da camada JPA. Repare na presença de um ficheiro [orm.xml], que ainda não tínhamos visto anteriormente. O projeto encontra-se em [3], dentro da pasta de exemplos [4]. Vamos importá-lo.

2.7.4. Gerar o DDL da base de dados

Seguindo as instruções da secção 2.1.7, o DDL resultante para o SGBD MySQL5 é o seguinte:


alter table jpa10_hb_restaurant 
        drop 
        foreign key FK3E8E4F5D5FE379D0;
 
    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D11F0F78A4;
 
    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D1AFAC3E44;
 
    drop table if exists jpa10_hb_adresse;
 
    drop table if exists jpa10_hb_plat;
 
    drop table if exists jpa10_hb_restaurant;
 
    drop table if exists jpa10_hb_restaurant_plat;
 
    create table jpa10_hb_adresse (
        id bigint not null auto_increment,
        NUMERO_RUE integer,
        NOM_RUE varchar(30) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_plat (
        id bigint not null auto_increment,
        nom varchar(50) not null unique,
        vegetarien bit not null,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa10_hb_restaurant (
        id bigint not null auto_increment,
        nom varchar(30) not null unique,
        adresse_id bigint,
        primary key (id)
    ) ENGINE=InnoDB;
 
    create table jpa10_hb_restaurant_plat (
        jpa10_hb_restaurant_id bigint not null,
        plat_id bigint not null,
        primary key (jpa10_hb_restaurant_id, plat_id)
    ) ENGINE=InnoDB;
 
    alter table jpa10_hb_restaurant 
        add index FK3E8E4F5D5FE379D0 (adresse_id), 
        add constraint FK3E8E4F5D5FE379D0 
        foreign key (adresse_id) 
        references jpa10_hb_adresse (id);
 
    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D11F0F78A4 (plat_id), 
        add constraint FK1D2D06D11F0F78A4 
        foreign key (plat_id) 
        references jpa10_hb_plat (id);
 
    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id), 
        add constraint FK1D2D06D1AFAC3E44 
        foreign key (jpa10_hb_restaurant_id) 
        references jpa10_hb_restaurant (id);
  • linhas 21-26: a tabela [address]
  • linhas 28-33: a tabela [dish]
  • linhas 35-40: a tabela [restaurant]
  • linhas 42-46: a tabela de junção [restaurant_dish]. Observe a chave composta (linha 45)
  • linhas 48-52: a chave estrangeira da tabela [restaurant] para a tabela [address]
  • linhas 54–58: a chave estrangeira da tabela [restaurant_dish] para a tabela [dish]
  • Linhas 60–64: a chave estrangeira da tabela [restaurant_dish] para a tabela [restaurant]

Este DDL corresponde ao esquema já apresentado:

Na vista do SQL Explorer, a base de dados apresenta-se da seguinte forma:

  • em [1]: as 4 tabelas da base de dados
  • em [2]: os endereços
  • em [3]: os pratos
  • em [4]: os restaurantes. [address_id] faz referência aos endereços de [2].
  • em [5]: a tabela de junção [restaurant,dish]. [jpa10_hb_restaurant_id] faz referência aos restaurantes em [4] e [dish_id] faz referência aos pratos em [3]. Assim, [1,1] significa que o restaurante «Burger Barn» serve o prato «CheeseBurger».

Para recuperar os dados acima, foi executado o programa [QueryDB] do projeto Eclipse.

2.7.5. Consultas JPQL com uma consola Hibernate

Criamos uma consola Hibernate ligada ao projeto Eclipse anterior. Seguiremos o procedimento já descrito duas vezes, nomeadamente na secção 2.1.12.

  • Em [1] e [2]: a configuração da consola Hibernate
  • em [3]: uma consulta JPQL e em [4] o resultado.
  • em [5]: a instrução SQL equivalente

Apresentaremos agora uma série de consultas JPQL. Convidamos o leitor a executá-las e a descobrir a instrução SQL gerada pelo Hibernate para as executar.

Obter todos os restaurantes com os seus pratos:

Obter restaurantes que servem pelo menos um prato vegetariano:

Obter os nomes dos restaurantes que servem apenas pratos vegetarianos:

Veja os restaurantes que servem hambúrgueres:

2.7.6. QueryDB

Vamos agora analisar o programa [QueryDB] do projeto Eclipse, que:

  • preenche a base de dados
  • e executa várias consultas JPQL nela. Estas estão armazenadas no ficheiro [META-INF/orm.xml] do projeto Eclipse:

O ficheiro [orm.xml] pode ser utilizado para configurar a camada JPA em vez de anotações Java. Isto proporciona flexibilidade na configuração da camada JPA. Pode ser modificado sem recompilar o código Java ou o [ . A configuração JPA é primeiro definida utilizando anotações Java e, em seguida, utilizando o ficheiro [orm.xml]. Portanto, se pretender modificar uma configuração definida por uma anotação Java sem recompilar, basta colocar essa configuração no [orm.xml]. Ela terá precedência.

No nosso exemplo, o ficheiro [orm.xml] é utilizado para armazenar textos de consultas JPQL. O seu conteúdo é o seguinte:


<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
    <description>Restaurants</description>
    <named-query name="supprimer le contenu de la table restaurant">
        <query>delete from Restaurant</query>
    </named-query>
    <named-query name="supprimer le contenu de la table plat">
        <query>delete from Plat</query>
    </named-query>
    <named-query name="obtenir tous les restaurants">
        <query>select r from Restaurant r order by r.nom asc</query>
    </named-query>
    <named-query name="obtenir toutes les adresses">
        <query>select a from Adresse a order by a.nomRue asc</query>
    </named-query>
    <named-query name="obtenir tous les plats">
        <query>select p from Plat p order by p.nom asc</query>
    </named-query>
    <named-query name="obtenir tous les restaurants avec leurs plats">
        <query>select r.nom,p.nom from Restaurant r join r.plats p</query>
    </named-query>
    <named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
        <query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
    </named-query>
    <named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
        <query>
            select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
            p1.vegetarien=false)
        </query>
    </named-query>
    <named-query name="obtenir les restaurants d'une certaine rue">
        <query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
    </named-query>
    <named-query name="obtenir les restaurants qui servent des burgers">
        <query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
    </named-query>
    <named-query name="obtenir les plats du restaurant untel">
        <query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
    </named-query>
</entity-mappings>
  • A raiz do ficheiro [orm.xml] é <entity-mappings> (linha 2).
  • Linhas 5–7: As consultas JPQL nomeadas estão entre as tags <named-query name="...">texto</named-query>.
    • O atributo name da tag é o nome da consulta.
    • O conteúdo de texto da tag é o texto da consulta.

O QueryDB irá executar as consultas anteriores. O seu código é o seguinte:


package tests;
 
...
public class QueryDB {
 
    // Persistence context
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
 
    private static EntityManager em = emf.createEntityManager();
 
    public static void main(String[] args) {
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // delete [restaurant] table items
        em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
        // delete table items [flat]
        em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
        // creation of Address objects
        Adresse adr1 = new Adresse(10, "Main Street");
        Adresse adr2 = new Adresse(20, "Main Street");
        Adresse adr3 = new Adresse(123, "Dover Street");
        // creation of Entree objects
        Plat ent1 = new Plat("Hamburger", false);
        Plat ent2 = new Plat("Cheeseburger", false);
        Plat ent3 = new Plat("Tofu Stir Fry", true);
        Plat ent4 = new Plat("Vegetable Soup", true);
        // creation of Restaurant objects
        Restaurant restaurant1 = new Restaurant();
        restaurant1.setNom("Burger Barn");
        restaurant1.setAdresse(adr1);
        restaurant1.getPlats().add(ent1);
        restaurant1.getPlats().add(ent2);
        Restaurant restaurant2 = new Restaurant();
        restaurant2.setNom("Veggie Village");
        restaurant2.setAdresse(adr2);
        restaurant2.getPlats().add(ent3);
        restaurant2.getPlats().add(ent4);
        Restaurant restaurant3 = new Restaurant();
        restaurant3.setNom("Dover Diner");
        restaurant3.setAdresse(adr3);
        restaurant3.getPlats().add(ent1);
        restaurant3.getPlats().add(ent2);
        restaurant3.getPlats().add(ent4);
        // persistence of Restaurant objects (and other objects through cascading)
        em.persist(restaurant1);
        em.persist(restaurant2);
        em.persist(restaurant3);
        // end transaction
        tx.commit();
        // dump base
        dumpDataBase();
        // end EntityManager
        em.close();
        // end EntityManagerFactory
        emf.close();
    }
 
    // database content display
    @SuppressWarnings("unchecked")
    private static void dumpDataBase() {
        // test2
        log("données de la base");
        // start of transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // restaurant displays
        log("[restaurants]");
        for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
            System.out.println(restaurant);
        }
        // address display
        log("[adresses]");
        for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
            System.out.println(adresse);
        }
        // flat displays
        log("[plats]");
        for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
            System.out.println(plat);
        }
        // displays links restaurants <--> dishes
        log("[restaurants/plats]");
        Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
        }
        log("[Liste des restaurants avec au moins un plat végétarien]");
        for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
            System.out.println(r);
        }
        // query
        log("[Liste des restaurants avec seulement des plats végétariens]");
        for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
            System.out.println(r);
        }
        // query
        log("[Liste des restaurants dans Dover Street]");
        for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
            System.out.println(r);
        }
        // query
        log("[Liste des restaurants ayant un plat de type burger]");
        record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
        }
        // query
        log("[Plats de Veggie Village]");
        for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
            System.out.println(r);
        }
        // end transaction
        tx.commit();
    }
 
    // logs
    private static void log(String message) {
        System.out.println(" -----------" + message);
    }
 
}

O resultado da execução de [QueryDB] é o seguinte:

-----------données de la base
 -----------[restaurants]
R[Burger Barn,A[10,Main Street],E[Cheeseburger,false],E[Hamburger,false]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
 -----------[adresses]
A[123,Dover Street]
A[10,Main Street]
A[20,Main Street]
 -----------[plats]
E[Cheeseburger,false]
E[Hamburger,false]
E[Tofu Stir Fry,true]
E[Vegetable Soup,true]
 -----------[restaurants/plats]
[Burger Barn,Cheeseburger]
[Burger Barn,Hamburger]
[Dover Diner,Cheeseburger]
[Dover Diner,Hamburger]
[Dover Diner,Vegetable Soup]
[Veggie Village,Tofu Stir Fry]
[Veggie Village,Vegetable Soup]
 -----------[Liste des restaurants avec au moins un plat végétarien]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants avec seulement des plats végétariens]
Veggie Village
 -----------[Liste des restaurants dans Dover Street]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants ayant un plat de type burger]
[Burger Barn,10,Main Street,Cheeseburger]
[Burger Barn,10,Main Street,Hamburger]
[Dover Diner,123,Dover Street,Cheeseburger]
[Dover Diner,123,Dover Street,Hamburger]
 -----------[Plats de Veggie Village]
Tofu Stir Fry
Vegetable Soup

Deixamos ao leitor a tarefa de estabelecer a ligação entre o código e os resultados. Para tal, recomendamos a execução das consultas JPQL na consola do Hibernate e a análise do código SQL correspondente.

Os leitores interessados encontrarão o projeto anterior implementado com o Toplink nos exemplos disponíveis para download com este tutorial:

O projeto Eclipse com Toplink é uma cópia do projeto Eclipse com Hibernate:

O ficheiro <persistence.xml> [2] declara as entidades geridas:


        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
            <!-- classes persistantes -->
        <class>entites.Restaurant</class>
        <class>entites.Adresse</class>
        <class>entites.Plat</class>
 
...
  • linhas 4-6: entidades geridas

As consultas JPQL armazenadas em [orm.xml] são executadas corretamente pelo TopLink. Para garantir isso, no projeto anterior tivemos o cuidado de não utilizar consultas HQL (Hibernate Query Language), que são, na verdade, um superconjunto do JPQL e cuja sintaxe não é totalmente suportada pelo JPQL.

2.8. Conclusão

Isto conclui a nossa visão geral das entidades JPA. Foi um processo demorado, mas alguns tópicos importantes (para o programador avançado) não foram abordados. Mais uma vez, recomendamos a leitura de um livro de referência, como o utilizado neste tutorial:

[ref1]: Java Persistence with Hibernate, de Christian Bauer e Gavin King, publicado pela Manning.