Skip to content

2. As entidades JPA

2.1. Exemplo 1 — Representação orientada a objetos de uma única tabela

2.1.1. A tabela [personne]

Consideremos um banco de dados com uma única tabela [personne], cuja função é armazenar algumas informações sobre indivíduos:

 
ID
chave primária da tabela
VERSION
versão do registro na tabela. Sempre que
a pessoa for alterada, seu número de versão é incrementado.
NOM
nome da pessoa
PRENOM
seu nome
DATENAISSANCE
data de nascimento
MARIE
número inteiro 0 (solteiro) ou 1 (casado)
NBENFANTS
número de filhos da pessoa

2.1.2. A entidade [Personne]

Estamos no seguinte ambiente de execução:

A camada JPA [5] deve servir de ponte entre o mundo relacional do banco de dados [7] e o mundo de objetos [4] manipulado pelos programas Java [3]. Essa ponte é estabelecida por meio de configuração, e há duas maneiras de fazê-lo:

  1. com arquivos XML. Essa era praticamente a única maneira de fazer isso até o advento do JDK 1.5
  1. com anotações Java a partir do JDK 1.5

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

O objeto [Personne], que representa a tabela [personne] 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;

    // construtores
    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 e setters
...
}

A configuração é feita por meio de anotações Java @Annotation. As anotações Java são interpretadas pelo compilador ou por ferramentas especializadas no momento da execução. Com exceção da anotação da linha 3, destinada ao compilador, todas as anotações aqui se destinam à implementação JPA utilizada, seja Hibernate ou Toplink. Portanto, elas serão interpretadas na execução. Na ausência de ferramentas capazes de interpretá-las, essas anotações são ignoradas. Assim, a classe [Personne] acima poderia ser utilizada em um contexto fora do JPA.

É preciso distinguir dois casos de uso das anotações JPA em uma classe C associada a uma tabela T:

  1. a tabela T já existe: as anotações JPA devem, nesse caso, reproduzir o que já existe (nome e definição das 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 lidar. Com a ajuda das anotações JPA, indicamos a estrutura da tabela T que desejamos. O caso 1 costuma ser mais complexo. A tabela T pode ter sido criada, há muito tempo, fora de qualquer contexto JPA. Sua estrutura pode, portanto, estar mal adaptada à ponte relacional/objeto de JPA. Para simplificar, vamos considerar o caso 2, em que a tabela T associada à classe C será criada com base nas anotações JPA da classe C.

Vamos comentar as anotações JPA da classe [Personne]:

  • linha 4: a anotação @Entity é a primeira anotação indispensável. Ela é colocada antes da linha que declara a classe e indica que a classe em questão deve ser gerenciada pela camada de persistência JPA. Na ausência dessa anotação, todas as outras anotações JPA seriam ignoradas.
  • linha 5: a anotação @Table designa a tabela do banco de dados da qual a classe é uma representação. Seu principal argumento é name, que designa o nome da tabela. Na ausência desse argumento, a tabela receberá o nome da classe, neste caso, [Personne]. Em nosso exemplo, a anotação @Table é, portanto, supérflua.
  • linha 8: a anotação @Id serve para indicar o campo na classe que corresponde à chave primária da tabela. Essa anotação é obrigatória. Ela indica, neste caso, que o campo id da linha 11 corresponde à chave primária da tabela.
  • linha 9: a anotação @Column serve para estabelecer a ligação entre um campo da classe e a coluna da tabela que esse campo representa. O atributo name indica o nome da coluna na tabela. Na ausência desse atributo, a coluna recebe o mesmo nome do campo. No nosso exemplo, o argumento name não era, portanto, obrigatório. O argumento nullable=false indica que a coluna associada ao campo não pode ter o valor NULL e que, portanto, o campo deve necessariamente ter um valor.
  • linha 10: a anotação @GeneratedValue indica como a chave primária é gerada quando criada automaticamente pelo SGBD. Esse será o caso em todos os nossos exemplos. Isso não é obrigatório. Assim, nossa pessoa poderia ter um número de estudante que serviria como chave primária e que não seria gerado pelo SGBD, mas definido pela aplicação. Nesse caso, a anotação @GeneratedValue estaria ausente. O argumento strategy indica como a chave primária é gerada quando produzida pelo SGBD. Nem todos os SGBD utilizam a mesma técnica para gerar os valores da 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, desempenha a função de um gerador de valores

A camada JPA deve gerar ordens SQL diferentes de acordo com os SGBD para criar o gerador de valores. Por meio da configuração, é indicado a ela o tipo de SGBD que ela deve gerenciar. Assim, ela pode saber qual é a estratégia habitual de geração de valores de chave primária desse SGBD. O argumento strategy = GenerationType.AUTO indica à camada JPA que ela deve utilizar essa estratégia habitual. Essa técnica funcionou em todos os exemplos deste documento para os sete SGBD utilizados.

  • linha 14: a anotação @Version identifica o campo utilizado para gerenciar acessos simultâneos a uma mesma linha da tabela.

Para compreender esse problema de acesso simultâneo a uma mesma linha da tabela [personne], suponhamos que um aplicativo web permita a atualização de uma pessoa e examinemos o seguinte caso:

No momento T1, um usuário U1 inicia a edição de um perfil de pessoa P. Nesse momento, o número de filhos é 0. Ele altera esse número para 1, mas antes que ele confirme sua alteração, um usuário U2 inicia a edição do mesmo perfil P. Como U1 ainda não confirmou sua alteração, U2 vê na tela que o número de filhos é 0. U2 altera o nome da pessoa P para letras maiúsculas. Em seguida, U1 e U2 confirmam suas alterações nessa ordem. É a alteração de U2 que prevalecerá: no banco de dados, o nome passará a estar em maiúsculas e o número de filhos permanecerá zero, mesmo que U1 acredite ter alterado esse valor para 1.

O conceito de versão de pessoa nos ajuda a resolver esse problema. Vamos retomar o mesmo caso de uso:

No momento T1, um usuário U1 inicia a edição de uma pessoa P. Nesse momento, o número de filhos é 0 e a versão é V1. Ele altera o número de filhos para 1, mas, antes de confirmar sua alteração, um usuário U2 inicia a edição da mesma pessoa P. Como U1 ainda não confirmou sua 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 letras maiúsculas. Em seguida, U1 e U2 validam suas alterações nessa ordem. Antes de validar uma alteração, verifica-se se quem está alterando a pessoa P possui a mesma versão que a pessoa P atualmente registrada. Esse será o caso do usuário U1. Sua alteração é, portanto, aceita e, então, altera-se a versão da pessoa modificada de V1 para V2 para indicar que a pessoa sofreu uma alteração. Ao validar a alteração de U2, perceberemos que U2 possui uma versão V1 da pessoa P, enquanto que, atualmente, a versão desta é V2. Será então possível informar ao usuário U2 que alguém já agiu antes dele e que ele deve partir da nova versão da pessoa P. Ele fará isso, recuperará uma pessoa P da versão V2, que agora tem um filho, colocará o nome em maiúsculas e validará. Sua modificação será aceita se a pessoa P registrada ainda tiver a versão V2. No final, as modificações feitas por U1 e U2 serão consideradas, enquanto que, no caso de uso sem versão, uma das modificações teria sido perdida.

A camada [dao] do aplicativo cliente pode gerenciar por conta própria a versão da classe [Personne]. Sempre que houver uma modificação em um objeto P, a versão desse objeto será incrementada em 1 na tabela. A anotação @Version permite transferir esse gerenciamento para a camada JPA. O campo em questão não precisa necessariamente se chamar version, como no exemplo. Ele pode ter qualquer nome.

Os campos correspondentes às anotações @Id e @Version estão presentes devido à persistência. Eles não seriam necessários se a classe [Personne] não precisasse ser persistida. Vemos, portanto, que um objeto não tem a mesma representação dependendo de precisar ou não ser persistido.

  • linha 17: novamente a anotação @Column para fornecer informações sobre a coluna da tabela [personne] associada ao campo nom da classe Personne. Encontramos aqui dois novos argumentos:
    • unique=true indica que o nome de uma pessoa deve ser único. Isso se traduzirá no banco de dados pela adição de uma restrição de exclusividade na coluna NOM da tabela [personne].
    • length=30 define em 30 o número de caracteres da coluna NOM. Isso significa que o tipo dessa coluna será VARCHAR(30).
  • linha 24: a anotação @Temporal serve para indicar qual tipo SQL deve ser atribuído a uma coluna/campo do tipo data/hora. O tipo TemporalType.DATE designa apenas 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 com hora.

Vamos agora comentar o restante do código da classe [Personne]:

  • linha 6: a classe implementa a interface Serializable. A sérialisation de um objeto consiste em transformá-lo em uma sequência de bits. A désérialisation é a operação inversa. A serialização/desserialização é utilizada, principalmente, em aplicações cliente/servidor, nas quais os objetos são trocados pela rede. As aplicações cliente ou servidor não têm conhecimento dessa operação, que é realizada de forma transparente pelos JVM. Para que isso seja possível, é necessário, no entanto, que as classes dos objetos trocados sejam “marcadas” com a palavra-chave Serializable.
  • linha 37: um construtor da classe. Observe-se que os campos id e version não fazem parte dos parâmetros. De fato, esses dois campos são gerenciados pela camada JPA e não pela aplicação.
  • linhas 51 e seguintes: os métodos get e set de cada um dos campos da classe. Vale ressaltar 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 JPA deve utilizar para acessar os campos:
    • se as anotações forem colocadas no nível do campo, o JPA acessará diretamente os campos para lê-los ou gravá-los
    • se as anotações forem colocadas no nível do get, o JPA acessará os campos por meio dos métodos get/set para lê-los ou gravá-los

É a posição da anotação @Id que determina a posição das anotações JPA em uma classe. Quando colocada no nível do campo, ela indica um acesso direto aos campos; quando colocada no nível get, indica um acesso aos campos por meio dos métodos get e set. As demais anotações devem, então, ser colocadas da mesma forma que a anotação @Id.

2.1.3. O projeto Eclipse dos testes

Realizaremos nossos primeiros testes com a entidade [Personne] mencionada anteriormente. Eles serão conduzidos com a seguinte arquitetura:

  • em [7]: o banco de dados que será gerado a partir das anotações da entidade [Personne], bem como de configurações complementares feitas em um arquivo chamado [persistence.xml]
  • em [5, 6]: uma camada JPA implementada pelo Hibernate
  • em [4]: a entidade [Personne]
  • em [3]: um programa de teste do tipo console

Faremos diversos testes:

  • gerar o esquema da BD a partir de um script Ant e da ferramenta Hibernate Tools
  • gerar a BD e inicializá-la com alguns dados
  • utilizar o BD e realizar as quatro operações básicas na tabela [personne] (inserção, atualização, exclusão, consulta)

As ferramentas necessárias são as seguintes:

  • Eclipse e seus plug-ins descritos no parágrafo 5.2.
  • o projeto [hibernate-personnes-entites], que se encontra na pasta <exemplos>/hibernate/direct/pessoas-entidades
  • os diversos arquivos SGBD descritos nos anexos (parágrafo 5 e seguintes).

O projeto do Eclipse é o seguinte:

  • em [1]: a pasta do projeto Eclipse
  • em [2]: o projeto importado para o Eclipse (Arquivo / Importar)
  • em [3]: a entidade [Personne] objeto dos testes
  • em [4]: os programas de teste
  • em [5]: [persistence.xml] é o arquivo de configuração da camada JPA
  • em [6]: as bibliotecas utilizadas. Elas foram descritas no parágrafo 1.5.
  • em [8]: um script ant que será utilizado para gerar a tabela associada à entidade [Personne]
  • em [9]: os arquivos [persistence.xml] para cada um dos SGBD utilizados
  • em [10]: os esquemas do banco de dados gerado para cada um dos SGBD utilizados

Descreveremos esses elementos um por um.

2.1.4. A entidade [Personne] (2)

Fazemos uma pequena alteração na descrição feita anteriormente da entidade [Personne], bem como um complemento de informação:


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;

    // construtores
    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 e setters
...
}
  • linha 7: atribuímos o nome [jpa01_personne] à tabela associada à entidade [Personne]. No documento, várias tabelas serão criadas em um esquema sempre chamado jpa. Ao final deste tutorial, o esquema jpa conterá várias tabelas. Para facilitar a orientação do leitor, as tabelas relacionadas entre si terão o mesmo prefixo jpaxx_.
  • linha 45: um método [toString] para exibir um objeto [Personne] no console.

2.1.5. Configuração da camada de acesso aos dados

No projeto Eclipse acima, a configuração da camada JPA é feita pelo arquivo [META-INF/persistence.xml]:

Durante a execução, o arquivo [META-INF/persistence.xml] é procurado no classpath do aplicativo. Em nosso projeto Eclipse, todo o conteúdo das pastas [/src] e [1] é copiado para as pastas [/bin] e [2]. Esta pasta faz parte do classpath do projeto. É por esse motivo que o [META-INF/persistence.xml] será encontrado quando a camada JPA for configurada.

Por padrão, o Eclipse não coloca os códigos-fonte na pasta [/src] do projeto, mas diretamente na própria pasta. Todos os nossos projetos do Eclipse serão configurados para que os códigos-fonte fiquem em [/src] e as classes compiladas em [/bin], conforme mostrado no parágrafo 5.2.1.

Vamos examinar a configuração da camada JPA definida no arquivo [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">
        <!-- provedor -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- classes persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexão 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" />
            <!--  criação automática do esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  propriedades 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 entender essa configuração, precisamos revisar a arquitetura de acesso aos dados do nosso aplicativo:

  • o arquivo [persistence.xml] configurará as camadas [4, 5, 6]
  • [4]: implementação do Hibernate para JPA
  • [5]: o Hibernate acessa o banco de dados por meio de um pool de conexões. Um pool de conexões é uma reserva de conexões abertas com o SGBD. Um SGBD é acessado por vários usuários, embora, por motivos de desempenho, não possa exceder um número limite N de conexões abertas simultaneamente. Um código bem escrito abre uma conexão com o SGBD pelo menor tempo possível: ele emite comandos SQL e fecha a conexão. Ele fará isso repetidamente, sempre que precisar trabalhar com o banco de dados. O custo de abrir e fechar uma conexão não é insignificante, e é aí que entra o pool de conexões. Ao iniciar a aplicação, o pool abrirá N1 conexões com o SGBD. É a ele que o aplicativo solicitará uma conexão aberta quando precisar. Essa conexão será devolvida ao pool assim que o aplicativo não precisar mais dela, de preferência o mais rápido possível. A conexão não é fechada e permanece disponível para o próximo usuário. Um pool de conexões é, portanto, um sistema de compartilhamento de conexões abertas.
  • [6]: o driver JDBC do SGBD utilizado

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

  • linha 2: a tag raiz do arquivo XML é <persistence>.
  • linha 3: <persistence-unit> serve para definir uma unidade de persistência. Pode haver várias unidades de persistência. Cada uma delas possui um nome (atributo name) e um tipo de transação (atributo transaction-type). O aplicativo terá acesso à unidade de persistência por meio do nome dela, neste caso, jpa. O tipo de transação RESOURCE_LOCAL indica que a própria aplicação gerencia as transações com o SGBD. Esse será o caso aqui. Quando a aplicação é executada em um contêiner EJB3, ela pode utilizar o serviço de transações desse contêiner. Nesse caso, definiremos transaction-type=JTA (Java Transaction API). JTA é o valor padrão quando o atributo transaction-type está ausente.
  • linha 5: a tag <provider> serve para definir uma classe que implementa a interface [javax.persistence.spi.PersistenceProvider], interface que permite que a aplicação inicialize a camada de persistência. Como estamos usando uma implementação JPA / Hibernate, a classe utilizada aqui é uma classe do Hibernate.
  • linha 6: a tag <properties> define propriedades específicas do provider escolhido. Assim, dependendo da escolha entre Hibernate, Toplink, Kodo, etc., teremos propriedades diferentes. As que se seguem são específicas do Hibernate.
  • linha 8: solicita ao Hibernate que explore o classpath do projeto para localizar as classes com a anotação @Entity, a fim de gerenciá-las. As classes @Entity também podem ser declaradas por meio das tags <class>nom_de_la_classe</class>, diretamente abaixo da tag <persistence-unit>. É isso que faremos com o provider JPA / Toplink.
  • As linhas 10 a 12, aqui colocadas como comentários, configuram os logs de console do Hibernate:
  • linha 10: para determinar se as ordens emitidas pelo Hibernate serão exibidas ou não no SGBD. Isso é muito útil durante a fase de aprendizado. Devido à ponte relacional/objeto, a aplicação trabalha com objetos persistentes aos quais aplica operações do tipo [persist, merge, remove]. É muito interessante saber quais são os comandos SQL realmente emitidos nessas operações. Ao analisá-los, aos poucos passamos a adivinhar os comandos SQL que o Hibernate irá gerar quando realizamos determinada operação nos objetos persistentes, e a ponte relacional/objeto começa a ganhar forma em nossa mente.
  • linha 11: as instruções SQL exibidas no console podem ser formatadas de maneira mais agradável para facilitar a leitura
  • linha 12: os comandos SQL exibidos serão, além disso, comentados
  • as linhas 15-19 definem a camada JDBC (camada [6] na arquitetura):
  • linha 15: a classe do driver JDBC do SGBD, neste caso MySQL5
  • linha 16: a URL do banco de dados utilizado
  • linhas 17 e 18: o usuário da conexão e sua senha
  • Utilizamos aqui elementos explicados nos anexos, no parágrafo 5.5. Recomenda-se ao leitor que leia essa seção sobre o MySQL5.
  • linha 22: O Hibernate precisa saber qual é o SGBD com o qual está trabalhando. De fato, todos os SGBD possuem extensões SQL proprietárias, uma forma específica de gerenciar a geração automática dos valores de uma chave primária, ... o que faz com que o Hibernate precise conhecer o SGBD com o qual está trabalhando, a fim de enviar a ele os comandos SQL que este compreenderá. [MySQL5InnoDBDialect] designa o SGBD MySQL5 com tabelas do tipo InnoDB que suportam transações.
  • as linhas 24-28 configuram o pool de conexões c3p0 (camada [5] na arquitetura):
  • linhas 24 e 25: o número mínimo (padrão 3) e máximo de conexões (padrão 15) no pool. O número inicial de conexões por padrão é 3.
  • linha 26: tempo máximo, em milissegundos, de espera por uma solicitação de conexão por parte do cliente. Após esse prazo, o c3p0 retornará uma exceção.
  • linha 27: para acessar a BD, o Hibernate utiliza ordens SQL preparadas (PreparedStatement) que o c3p0 pode armazenar em cache. Isso significa que, se a aplicação solicitar pela segunda vez uma ordem SQL preparada que já esteja no cache, ela não precisará ser preparada novamente (a preparação de uma ordem SQL tem um custo) e a que está no cache será utilizada. Aqui, indica-se o número máximo de ordens SQL preparadas que o cache pode conter, considerando todas as conexões (uma ordem SQL preparada pertence a uma conexão).
  • linha 28: frequência de verificação, em milissegundos, da validade das conexões. Uma conexão do pool pode se tornar inválida por diversos motivos (o driver JDBC invalida a conexão porque ela está demorando demais, o driver JDBC apresenta “bugs”, etc.).
  • linha 20: aqui, solicita-se que, na inicialização da unidade de persistência, seja gerado o esquema do banco de dados dos objetos @Entity. O Hibernate agora dispõe de todas as ferramentas para emitir os comandos SQL de geração das tabelas do banco de dados:
  • a configuração dos objetos @Entity permite que ele identifique as tabelas a serem geradas
  • as linhas 15-18 e 24-28 permitem que ele obtenha uma conexão com o SGBD
  • a linha 22 permite que ele saiba qual dialeto SQL usar para gerar as tabelas

Assim, o arquivo [persistence.xml] utilizado aqui recria um banco de dados novo a cada nova execução do aplicativo. As tabelas são recriadas (create table) após terem sido excluídas (drop table), caso já existissem. Vale ressaltar que isso obviamente não deve ser feito em um banco de dados em produção...

Os testes demonstraram que a fase de drop/create das tabelas poderia falhar. Isso ocorreu, em particular, quando, para um mesmo teste, passávamos de uma camada JPA/Hibernate para uma camada JPA/Toplink ou vice-versa. A partir dos mesmos objetos @Entity, as duas implementações não geram exatamente as mesmas tabelas, geradores, sequências, etc., e, às vezes, a fase de exclusão/criação falhava, obrigando-nos a excluir as tabelas manualmente. A seção “Anexos”, parágrafo 5 e seguintes, descreve os aplicativos que podem ser utilizados para realizar esse trabalho manualmente. Vale ressaltar que a implementação JPA/Hibernate se mostrou a mais eficiente nessa fase inicial de criação do conteúdo do banco de dados: foram raras as falhas no sistema.

As ferramentas utilizadas pela camada JPA / Hibernate estão na biblioteca [jpa-hibernate], apresentada no parágrafo 1.5, página 8. Os drivers JDBC necessários para acessar os SGBD estão na biblioteca [jpa-divers]. Essas duas bibliotecas foram incluídas no classpath do projeto aqui estudado. Apresentamos a seguir seu conteúdo:

2.1.6. Geração do banco de dados com um script Ant

Como acabamos de ver, o Hibernate fornece ferramentas para gerar o esquema do banco de dados dos objetos @Entity do aplicativo. O Hibernate pode:

  • gerar o arquivo de texto com os comandos SQL para criar o banco de dados. Nesse caso, apenas o dialeto definido em [persistence.xml] é utilizado.
  • criar as tabelas correspondentes aos objetos @Entity no banco de dados de destino definido no [persistence.xml]. Nesse caso, todo o arquivo [persistence.xml] é utilizado.

Apresentaremos um script Ant capaz de gerar o esquema do banco de dados e as tabelas dos objetos @Entity. Este script não é de minha autoria: ele se baseia em um script semelhante do [ref1]. O Ant (Another Neat Tool) é uma ferramenta de execução em lote de tarefas Java. Os scripts Ant não são fáceis de entender para quem está começando. Usaremos apenas um deles, aquele que comentaremos agora:

  • em [1]: a estrutura de diretórios dos exemplos deste tutorial.
  • em [2]: a pasta [personnes-entites] do projeto Eclipse atualmente em estudo
  • em [3]: a pasta <lib> contendo as cinco bibliotecas JAR definidas no parágrafo 1.5.
  • em [4]: o arquivo [hibernate-tools.jar] necessário para uma das tarefas do script [ant-hibernate.xml] que vamos estudar.
  • 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 arquivos JAR da pasta <lib> [3], especialmente o arquivo [hibernate-tools.jar] [4] da pasta [lib/hibernate]. Reproduzimos a estrutura de pastas para que o leitor perceba que, para encontrar a pasta [lib] a partir da pasta [personnes-entites] [2] do script [ant-hibernate.xml], é preciso seguir o caminho: ../../../lib.

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


<project name="jpa-hibernate" default="compile" basedir=".">

    <!-- nome do projeto e versão -->
    <property name="proj.name" value="jpa-hibernate" />
    <property name="proj.shortname" value="jpa-hibernate" />
    <property name="version" value="1.0" />

    <!-- Propriedades globais -->
    <property name="src.java.dir" value="src" />
    <property name="lib.dir" value="../../../lib" />
    <property name="build.dir" value="bin" />

    <!-- o Classpath do projeto -->
    <path id="project.classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <!-- os arquivos de configuração que devem estar no classpath-->
    <patternset id="conf">
        <include name="**/*.xml" />
        <include name="**/*.properties" />
    </patternset>

    <!-- Limpeza do projeto -->
    <target name="clean" description="Nettoyer le projet">
        <delete dir="${build.dir}" />
        <mkdir dir="${build.dir}" />
    </target>

    <!-- Compilação do projeto -->
<target name="compile" depends="clean">
        <javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
    </target>

    <!-- Copiar os arquivos de configuração para o classpath -->
    <target name="copyconf">
        <mkdir dir="${build.dir}" />
        <copy todir="${build.dir}">
            <fileset dir="${src.java.dir}">
                <patternset refid="conf" />
            </fileset>
        </copy>
    </target>

    <!-- Ferramentas do Hibernate -->
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />

    <!-- Gerar o DDL do banco de dados -->
    <target name="DDL" depends="compile, copyconf" description="Génération DDL base">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Usar META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- exportar -->
            <hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>

    <!-- Gerar a base -->
    <target name="BD" depends="compile, copyconf" description="Génération BD">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Usar META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- exportar -->
            <hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
</project>
  • linha 1: o projeto [ant] é chamado de “jpa-hibernate”. Ele reúne um conjunto de tarefas, sendo que uma delas é a tarefa padrão: neste caso, a tarefa chamada “compile”. Um script ant é chamado para executar uma tarefa T. Se essa tarefa não for especificada, a tarefa padrão será executada. basedir="." indica que, para todos os caminhos relativos encontrados no script, o ponto de partida é a pasta na qual se encontra o script ant, neste caso a pasta <exemplos>/hibernate/direct/pessoas-entidades.
  • linhas 3-11: definem variáveis de script com a tag <property name="nomVariable" value="valeurVariable"/>. A variável pode então ser utilizada no script com a notação ${nomVariable}. Os nomes podem ser quaisquer. Vamos nos deter nas variáveis definidas nas linhas 9 a 11:
    • linha 9: define uma variável chamada “src.java.dir” (o nome é livre) que, no restante do script, indicará a pasta que contém os códigos-fonte Java. Seu valor é “src”, um caminho relativo à pasta indicada pelo atributo basedir (linha 1). Trata-se, portanto, do caminho “./src”, onde . designa aqui a pasta <exemplos>/hibernate/direct/pessoas-entidades. É precisamente na pasta <pessoas-entidades>/src que se encontram os códigos-fonte Java (cf. [6] acima).
    • linha 10: define uma variável chamada “lib.dir” que, no restante do script, indicará a pasta que contém os arquivos JAR necessários para as tarefas Java do script. Seu valor ../../../lib indica a pasta <exemplos>/lib (ver [3] acima).
    • linha 11: define uma variável chamada “build.dir” que, no restante do script, indicará a pasta onde devem ser gerados os arquivos .class resultantes da compilação dos códigos-fonte .java. Seu valor “bin” indica a pasta <pessoas-entidades>/bin. Já explicamos que, no projeto Eclipse analisado, a pasta <bin> era aquela onde os arquivos .class eram gerados. O Ant fará o mesmo.
    • linhas 14-18: a tag <path> serve para definir elementos do classpath que deverão ser utilizados pelas tarefas ant. Aqui, o caminho “project.classpath” (o nome é livre) reúne todos os arquivos .jar da árvore de diretórios <exemples>/lib.
    • linhas 21-24: a tag <patternset> serve para designar um conjunto de arquivos por meio de padrões de nomes. Aqui, o patternset, denominado conf, designa todos os arquivos com a extensão .xml ou .properties. Este patternset servirá para designar os arquivos .xml e .properties da pasta <src> (persistence.xml, log4j.properties) (ver [6]), que são arquivos de configuração do aplicativo. No momento da execução de determinadas tarefas, esses arquivos devem ser copiados para a pasta <bin> para que fiquem no diretório classpath do projeto. Utilizaremos, então, o patternset conf para identificá-los.
    • linhas 27-30: a tag <target> designa uma tarefa do script. É a primeira que encontramos. Tudo o que veio antes diz respeito à configuração do ambiente de execução do script ant. A tarefa se chama clean. Ela é executada em duas etapas: a pasta <bin> é excluída (linha 28) para, em seguida, ser 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`. Isso significa que, antes de executar a tarefa “compile”, o ant deve executar a tarefa “clean”, c.a.d, para limpar a pasta <bin>. O objetivo da tarefa “compile” aqui é compilar os códigos-fonte Java da pasta <src>.
    • linha 34: chamada do compilador Java com três parâmetros:
      • srcdir: a pasta que contém os códigos-fonte Java, neste caso a pasta <src>
      • destdir: a pasta onde os arquivos .class gerados devem ser armazenados, neste caso a pasta <bin>
      • classpathref: o classpath a ser usado para a compilação, neste caso, todos os arquivos jar da árvore de diretórios da pasta <lib>
  • (continuação)
    • linhas 38-45: a tarefa copyconf, cujo objetivo é copiar para a pasta <bin> todos os arquivos .xml e .properties da pasta <src>.
    • linha 48: definição de uma tarefa por meio da tag <taskdef>. Essa tarefa destina-se a ser reutilizada em outras partes do script. Trata-se de uma facilidade de codificação. Como a tarefa é utilizada em diversos pontos do script, ela é definida uma única vez com a tag <taskdef> e, posteriormente, reutilizada por meio de seu nome, sempre que necessário.
      • A tarefa se chama hibernatetool (atributo name).
      • Sua classe é definida pelo atributo classname. Aqui, a classe indicada será encontrada no arquivo [hibernate-tools.jar], do qual já falamos.
      • O atributo classpathref indica ao ant onde procurar a classe anterior
  • (continuação)
    • as linhas 51-60 referem-se à tarefa que nos interessa aqui, a geração do esquema do banco de dados de imagem dos objetos @Entity do nosso projeto Eclipse.
      • linha 51: a tarefa se chama DDL (como Data Definition Language, o SQL associado à criação de objetos de um banco de dados). Ela depende das tarefas compile e copyconf, nessa ordem. A tarefa DDL irá, portanto, provocar, nessa ordem, a execução das tarefas clean, compile e copyconf. Quando a tarefa DDL é iniciada, a pasta <bin> contém os arquivos .class das fontes .java, notadamente dos objetos @Entity, bem como o arquivo [META-INF/persistence.xml], que configura a camada JPA / Hibernate.
      • linhas 53-59: a tarefa [hibernatetool] definida na linha 48 é chamada. São passados a ela vários parâmetros, além daqueles já definidos na linha 48:
      • linha 53: a pasta de saída dos resultados gerados pela tarefa será a pasta atual.
      • linha 54: a pasta <bin> será a pasta de saída da tarefa classpath
      • linha 56: indica à tarefa [hibernatetool] como ela pode identificar seu ambiente de execução: a tag <jpaconfiguration/> indica que ela está em um ambiente JPA e que, portanto, deve utilizar o arquivo [META-INF/persistence.xml], que encontrará aqui em seu classpath.
      • a linha 58 define as condições de geração do banco de dados: drop=true indica que os comandos SQL drop table devem ser emitidos antes da criação das tabelas; create=true indica que o arquivo de texto com os comandos SQL para a criação do banco de dados deve ser criado; outputfilename indica o nome desse arquivo SQL — neste caso, schema.sql — na pasta <ddl> do projeto Eclipse; export=false indica que os comandos SQL gerados não devem ser executados em uma conexão com o SGBD. Esse ponto é importante: ele implica que, para executar a tarefa, o SGBD de destino não precisa ser iniciado. delimiter define o caractere que separa dois comandos SQL no esquema gerado; format=true solicita que seja feita uma formatação básica no texto gerado.
  • (continuação)
    • as linhas 63-72 definem a tarefa denominada BD. Ela é idêntica à tarefa anterior DDL, exceto que, desta vez, ela gera o banco de dados (export="true" na linha 70). A tarefa abre uma conexão com a tarefa SGBD usando as informações encontradas na tarefa [persistence.xml], para executar o esquema SQL e gerar o banco de dados. Para executar a tarefa BD, é necessário, portanto, que a tarefa SGBD seja iniciada.

2.1.7. Execução da tarefa antes da tarefa DDL

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

  • em [1]: selecionar [External Tools]
  • em [2]: criar uma nova configuração ant
  • em [3]: nomear a configuração ant
  • em [5]: selecionar o script ant usando o botão [4]
  • em [6]: aplicar as alterações
  • em [7]: foi criada a configuração ant DDL
  • em [8]: na aba JRE, define-se o JRE a ser utilizado. O campo [10] normalmente vem preenchido com o JRE utilizado pelo Eclipse. Portanto, normalmente não há nada a ser feito nesse painel. No entanto, encontrei um caso em que o script ant não conseguia localizar o compilador <javac>. Este não está em um JRE (Java Runtime Environment), mas em um JDK (Java Development Kit). A ferramenta ant do Eclipse localiza esse compilador por meio da variável de ambiente JAVA_HOME (Iniciar / Painel de Controle / Desempenho e Manutenção / Sistema / guia Avançado / botão Variáveis de ambiente) [A]. Se essa variável não tiver sido definida, é possível permitir que o ant localize o compilador <javac> inserindo no [10], não um JRE, mas um JDK. Este está disponível na mesma pasta que o JRE e o [B]. Usaremos o botão [9] para declarar o JDK entre os JRE disponíveis e o [C], a fim de poder selecioná-lo posteriormente no [10].
  • em [12]: na aba [Targets], seleciona-se a tarefa DDL. Assim, a configuração ant, que chamamos de DDL [7], corresponderá à execução da tarefa chamada DDL [12], que, como sabemos, gera o esquema DDL do banco de dados de imagens dos objetos @Entity do aplicativo.
  • em [13]: valida-se a configuração
  • em [14]: executa-se

Na visualização [console], obtêm-se os logs da execução da tarefa ant DDL:


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
  • lembramos que a tarefa DDL tem o nome [hibernatetool] (linha 10) e que depende das tarefas clean (linha 2), compile (linha 5) e copyconf (linha 7).
  • linha 10: a tarefa [hibernatetool] utiliza o arquivo [persistence.xml] de uma configuração JPA
  • linha 11: a tarefa [hbm2ddl] irá gerar o esquema DDL do banco de dados
  • linhas 12-22: o esquema DDL do banco de dados

Lembramos que solicitamos à tarefa [hbm2ddl] que gerasse o esquema DDL em um 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 arquivo ddl/schema.sql. Vamos verificar:
  • em [1]: o arquivo ddl/schema.sql está presente (execute F5 para atualizar a árvore de diretórios)
  • em [2]: seu conteúdo. Trata-se do esquema de um banco de dados MySQL5. O arquivo de configuração [persistence.xml] da camada JPA especificava, de fato, um SGBD MySQL5 (linha 8 abaixo):


            <!-- conexão JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
            <!-- criação automática do esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  propriedades DataSource c3p0 -->
...

Vamos examinar a ponte entre objeto e relacionamento que foi criada aqui, analisando a configuração do objeto @Entity Pessoa e o esquema DDL gerado:

Vale destacar alguns pontos:

  • A1-B1: o nome da tabela especificado em A1 é, de fato, o mesmo utilizado em B1. Observe-se que o drop precede o create no B1.
  • A2-B2: mostra o modo de geração da chave primária. O modo AUTO especificado em A2 resultou no atributo autoincrement específico de MySQL5. O modo de geração da chave primária é, na maioria das vezes, específico do SGBD.
  • A3-B3: exibe o tipo SQL, cujo bit é específico do MySQL5, para representar um tipo boolean em Java.

Vamos repetir esse teste com outro SGBD:

  • A pasta [conf] [1] contém os arquivos [persistence.xml] para diversos SGBD. Tomemos como exemplo o arquivo da Oracle [2] e coloquemo-lo na pasta [META-INF] [3] no lugar do anterior. 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">
        <!--  provedor -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!--  Classes persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexão 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" />
            <!--  criação automática do esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  propriedades 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 ao leitor que consulte, nos anexos, a seção sobre o Oracle (parágrafo 5.7), especialmente para compreender a configuração JDBC.

Apenas a linha 25 é realmente importante aqui: indica-se ao Hibernate que, a partir de agora, o SGBD é um SGBD Oracle. A execução da tarefa ant DDL produz o resultado [4] acima. Observe-se que o esquema Oracle é diferente do esquema MySQL5. Esse é um ponto forte do JPA: o desenvolvedor não precisa se preocupar com esses detalhes, o que aumenta consideravelmente a portabilidade de seus desenvolvimentos.

2.1.8. Execução da tarefa ant BD

Talvez nos lembremos de que a tarefa ant, denominada BD, faz o mesmo que a tarefa ant DDL, mas, além disso, gera o banco de dados. Portanto, é necessário que a tarefa SGBD seja executada. Vamos considerar o caso das tarefas SGBD e MySQL5 e convidamos o leitor a copiar o arquivo [conf/mysql5/persistence.xml] para a pasta [src/META-INF]. Para verificar o funcionamento da tarefa, vamos utilizar o plug-in SQL Explorer (ver parágrafo 5.2.6) para verificar o estado do arquivo jpa BD antes e depois da execução da tarefa ant BD.

Em primeiro lugar, precisamos criar uma nova configuração ant para executar a tarefa BD. Recomenda-se ao leitor que siga o procedimento descrito para a configuração anterior DDL no parágrafo 2.1.7. A nova configuração ant será denominada BD:

  • em [1]: duplica-se a configuração anterior denominada DDL
  • em [2]: a nova configuração passa a se chamar BD. Ela executa a tarefa ant BD [3], que gera fisicamente o banco de dados.
  • Feito isso, execute o SGBD e o MySQL5 (parágrafo 5.5).

Agora, utilizamos o plug-in SQL Explorer para explorar as bases de dados gerenciadas pelo SGBD. O leitor deve, previamente, familiarizar-se com este plug-in, se necessário (ver parágrafo 5.2.6).

  • [1]: abre-se a perspectiva SQL Explorer [Window / Open Perspective / Other]
  • [2]: crie, se necessário, uma conexão [mysql5-jpa] (consulte o parágrafo 5.5.5, página 252) e abra-a
  • [3]: faz-se o login com jpa / jpa
  • [4]: estamos conectados ao MySQL5.
  • em [5]: o BD jpa possui apenas uma tabela: [articles]
  • em [6]: iniciamos a execução da tarefa ant BD. Como estamos na perspectiva [SQL Explorer], não vemos a visualização [Console], que nos mostra os logs da tarefa. É possível exibir essa visualização [Window / Show View / ...] ou retornar à perspectiva Java [Window / Open Perspective / ...].
  • Na [7]: assim que a tarefa ant BD for concluída, retorne, se necessário, à perspectiva [SQL Explorer] e atualize a árvore da BD jpa.
  • em [8]: é possível ver a tabela [jpa01_personne] que foi criada.

Sugere-se ao leitor que repita essa geração de BD com outros SGBD. O procedimento a ser seguido é o seguinte:

  • copie o arquivo [conf/<sgbd>/persistence.xml] para a pasta [src/META-INF], onde <sgbd> é o SGBD testado
  • execute <sgbd> seguindo as instruções dos anexos referentes a ele
  • no SQL Explorer, criar uma conexão com <sgbd>. Isso também é explicado nos anexos para cada um dos SGBD
  • repita os testes anteriores

Chegando até aqui, já adquirimos alguns conhecimentos:

  • compreendemos melhor o conceito de ponte objeto-relacional. Aqui, ela foi implementada pelo Hibernate. Mais adiante, utilizaremos o Toplink.
  • sabemos que essa ponte objeto-relacional é configurada em dois locais:
  • nos objetos @Entity, onde indicamos as ligações entre os campos dos objetos e as colunas das tabelas do BD
  • no [META-INF/persistence.xml], onde fornecemos à implementação JPA informações sobre os dois elementos da ponte objeto/relacional: os objetos @Entity (objeto) e o banco de dados (relacional).
  • Criamos duas tarefas Ant, chamadas DDL e BD, que nos permitem criar o banco de dados a partir da configuração anterior, antes mesmo de escrever qualquer código Java.

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

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

Vamos explicar um pouco o ambiente de execução de um cliente JPA:

Sabemos que a camada JPA [2] cria uma ponte objeto [3] / relacional [4]. Denomina-se “contexto de persistência” o conjunto de objetos gerenciados pela camada JPA no âmbito dessa ponte objeto/relacional. Para acessar os dados do contexto de persistência, um cliente JPA [1] deve passar pela camada JPA [2]:

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

A camada JPA apresenta ao cliente uma interface chamada [EntityManager] que, como o próprio nome indica, permite gerenciar os objetos @Entity do contexto de persistência. Apresentamos a seguir os principais métodos dessa interface:

void persist(Object entity)
insere entity no contexto de persistência
void remove(Object entity)
remove entity do contexto de persistência
<T> T merge(T entity)
mescla um objeto entity do cliente, não gerenciado pelo contexto de persistência,
com o objeto entity do contexto de persistência que possui a mesma chave primária.
O resultado gerado é o objeto entity do contexto de persistência.
<T> T find(Class<T> entityClass,
 Object primaryKey)
insere, no contexto de persistência, um objeto buscado no banco de dados
por meio de sua chave primária. O tipo T do objeto permite
que a camada JPA saiba qual tabela consultar.
O objeto persistente assim criado é retornado 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)
método semelhante ao anterior, com a diferença de que queryText é,
uma ordem SQL e não JPQL.
Query createNamedQuery(String name)
método idêntico ao de createQuery, exceto que a ordem JPQL queryText foi
foi externalizado para um arquivo de configuração e associado a um nome.
É esse nome que constitui o parâmetro do método.

Um objeto EntityManager possui um ciclo de vida que não é necessariamente o mesmo da aplicação. Ele 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 ele. Eles são indissociáveis um do outro. Quando um objeto EntityManager é fechado, seu contexto de persistência é, se necessário, sincronizado com o banco de dados e, em seguida, deixa de existir. É necessário criar um novo EntityManager para dispor novamente de um contexto de persistência.

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


        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  • javax.persistence.Persistence é uma classe estática que permite obter uma fábrica (factory) de objetos EntityManager. Essa fábrica está vinculada a uma unidade de persistência específica. Vale lembrar que o arquivo de configuração [META-INF/persistence.xml] permite definir unidades de persistência e que estas possuem um nome:

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

No exemplo acima, a unidade de persistência se chama jpa. Junto com ela, vem toda uma configuração específica, incluindo o SGBD com o qual ela opera. A instrução [Persistence.createEntityManagerFactory("jpa")] cria uma fábrica de objetos do tipo EntityManagerFactory capaz de fornecer objetos EntityManager destinados a gerenciar contextos de persistência vinculados à unidade de persistência denominada jpa. A obtenção de um objeto EntityManager e, portanto, de um contexto de persistência, é feita a partir do objeto EntityManagerFactory da seguinte maneira:

        EntityManager em = emf.createEntityManager();

Os seguintes métodos da interface [EntityManager] permitem gerenciar 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 o banco de dados:
  • se um objeto do contexto não estiver presente no banco de dados, ele é inserido por meio de uma operação (SQL INSERT)
  • se um objeto do contexto estiver presente no banco de dados e tiver sido modificado desde que foi lido, é executada uma operação SQL UPDATE para persistir a modificação
  • se um objeto do contexto tiver sido marcado como “excluído” após a execução de uma operação remove sobre ele, é executada uma operação SQL DELETE para excluí-lo do banco de dados.
void clear()
O contexto de persistência é esvaziado de todos os seus objetos, mas não é fechado.
void flush()
o contexto de persistência é sincronizado com o banco de dados da maneira descrita para close()

O cliente JPA pode forçar a sincronização do contexto de persistência com o banco de dados usando o método [EntityManager].flush anterior. A sincronização pode ser explícita ou implícita. No primeiro caso, cabe ao cliente executar as operações flush quando desejar realizar sincronizações; caso contrário, elas ocorrem em determinados momentos que especificaremos a seguir. O modo de sincronização é gerenciado 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 feita no banco de dados.
FlushModeType.COMMIT: a sincronização ocorre somente ao final das transações no banco de dados.
FlushModeType getFlushMode()
define o modo atual de sincronização

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

  1. antes de cada operação SELECT, com base
  2. ao final de uma transação na base
  3. após uma operação flush ou close no contexto de persistência

No modo FlushModeType.COMMIT, ocorre o mesmo, exceto pela operação 1, que não é executada. O modo normal de interação com a camada JPA é um modo transacional. O cliente realiza diversas operações no contexto de persistência, dentro de uma transação. Nesse caso, os momentos de sincronização do contexto de persistência com o banco de dados correspondem aos casos 1 e 2 acima no modo AUTO, e apenas ao caso 2 no modo COMMIT.

Concluímos com o API da interface Query, que permite emitir ordens JPQL no contexto de persistência ou ordens SQL diretamente no banco de dados para recuperar dados. A interface Query é a seguinte:

Teremos que utilizar os métodos 1 a 4 acima:

  • 1 - o método getResultList executa um SELECT que retorna vários objetos. Esses objetos serão obtidos em um objeto List. Esse objeto é uma interface. Ela oferece um objeto Iterator que permite percorrer os elementos da lista L da seguinte forma:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
             // processar o objeto iterator.next() que representa o elemento atual da lista
...
}

A lista L também pode ser explorada com um for:


        for (Object o : L) {
             // utilizar o objeto o
}
  • 2 - o método getSingleResult executa um comando JPQL / SQL / SELECT que retorna um único objeto.
  • 3 - o método executeUpdate executa uma ordem SQL de atualização ou exclusão e retorna 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 de uma ordem JPQL configurada
  • 5 - o método setParameter(int, Object), porém o parâmetro não é designado pelo nome, mas pela posição na ordem JPQL.

2.1.10. Um primeiro cliente JPA

Voltemos à perspectiva Java do projeto:

 

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

  • [InitDB.java] é um programa que insere algumas linhas na tabela [jpa01_personne] do banco de dados. Seu código nos fornecerá os primeiros elementos da camada JPA.
  • [Main.java] é um programa que executa as operações CRUD na tabela [jpa01_personne]. A análise de seu código nos permitirá abordar os conceitos fundamentais do contexto de persistência e do ciclo de vida dos objetos 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 {
    // constantes
    private final static String TABLE_NAME = "jpa01_personne";

    public static void main(String[] args) throws ParseException {
        // Unidade de persistência
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        // recuperar um EntityManagerFactory a partir da unidade de persistência
        EntityManager em = emf.createEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // excluir os elementos da tabela de pessoas
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // criar duas pessoas
        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);
        // persistência de pessoas
        em.persist(p1);
        em.persist(p2);
        // exibição de pessoas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fim da transação
        tx.commit();
        // fim de EntityManager
        em.close();
        // fim de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé ...");
    }
}

É preciso ler esse código à luz do que foi explicado no parágrafo 2.1.9.

  • linha 19: solicita-se um objeto EntityManagerFactory emf para a unidade de persistência jpa (definida em persistence.xml). Essa operação normalmente é realizada apenas uma vez durante a vida útil de uma aplicação.
  • linha 21: solicita-se um objeto EntityManager do tipo em para gerenciar um contexto de persistência.
  • linha 23: solicita-se um objeto Transaction para gerenciar uma transação. Vale lembrar aqui que as operações no contexto de persistência são realizadas dentro de uma transação. Veremos que isso não é obrigatório, mas que, nesse caso, podem surgir problemas. Se a aplicação for executada em um contêiner EJB3, as operações no contexto de persistência serão sempre realizadas dentro de uma transação.
  • linha 24: a transação é iniciada
  • linha 26: executa um comando SQL DELETE na tabela “jpa01_personne” (nativeQuery). Fazemos isso para esvaziar a tabela de todo o conteúdo e, assim, visualizar melhor o resultado da execução do aplicativo [InitDB]
  • linhas 28-29: são criados dois objetos Personne, p1 e p2. Trata-se de objetos normais e, por enquanto, não têm nada a ver com o contexto de persistência. Em relação ao contexto de persistência, o Hibernate afirma que esses objetos estão em um estado transitório (transient), em contraste com os objetos persistentes (persistent), que são gerenciados pelo contexto de persistência. Preferimos falar de objetos não persistentes (expressão não francesa) para indicar que eles ainda não são gerenciados pelo contexto de persistência e de objetos persistentes para aqueles que são gerenciados por ele. Encontraremos uma terceira categoria de objetos, os objetos desanexados (detached), que são objetos anteriormente persistentes, mas cujo contexto de persistência foi fechado. O cliente pode manter referências a tais objetos, o que explica por que eles não são necessariamente destruídos ao fechar o contexto de persistência. Diz-se, então, que eles estão no estado “desvinculado”. A operação [EntityManager].merge permite vinculá-los novamente a um contexto de persistência recém-criado.
  • linhas 31-32: as pessoas p1 e p2 são integradas ao contexto de persistência pela operação [EntityManager].persist. Elas passam, então, a ser objetos persistentes.
  • linhas 35-37: é executada uma ordem JPQL “select p from Pessoa p order by p.nom asc”. Personne não é a tabela (que se chama jpa01_personne), mas o objeto @Entity associado à tabela. Trata-se aqui de uma consulta JPQL (Java Persistence Query Language) no contexto de persistência e não de uma ordem SQL no banco de dados. Dito isso, com exceção do objeto Personne, que substituiu a tabela jpa01_personne, as sintaxes são idênticas. Um loop for percorre a lista (de pessoas) resultante da consulta select para exibir cada elemento no console. O objetivo aqui é verificar se os elementos inseridos no contexto de persistência nas linhas 31-32 realmente constam na tabela. De forma transparente, ocorrerá uma sincronização do contexto de persistência com o banco de dados. De fato, uma consulta select será emitida, e já foi mencionado que esse é um dos casos em que a sincronização é realizada. Portanto, é nesse momento que, em segundo plano, o JPA / Hibernate emitirá os dois comandos SQL e insert, que inserirão as duas pessoas na tabela jpa01_personne. A operação persist não havia feito isso. Essa operação integra objetos no contexto de persistência sem que isso tenha qualquer consequência no banco de dados. As ações efetivas ocorrem durante as sincronizações, neste caso, logo antes da operação select no banco de dados.
  • linha 39: encerra-se a transação iniciada na linha 24. Uma sincronização ocorrerá novamente. Nada acontecerá aqui, pois o contexto de persistência não mudou desde a última sincronização.
  • linha 41: fecha-se o contexto de persistência.
  • linha 43: fecha-se a fábrica de EntityManager.

2.1.10.2. A execução do código

  • executar o SGBD MySQL5
  • colocar conf/mysql5/persistence.xml em META-INF/persistence.xml, se necessário
  • executar o aplicativo [InitDB]

Obtêm-se os seguintes resultados:

  • em [1]: a exibição do console na perspectiva Java. Obtém-se o resultado esperado.
  • em [2]: verifica-se o conteúdo da tabela [jpa01_personne] com a perspectiva SQL Explorer, conforme explicado no parágrafo 2.1.8. É possível observar dois pontos:
    • a chave primária ID foi gerada automaticamente
    • o mesmo vale para o número da versão. Observa-se que a primeira versão tem o número 0..

Temos aqui os primeiros elementos da cultura JPA. Conseguimos inserir dados em uma tabela. Vamos aproveitar esses conhecimentos para escrever o segundo teste, mas antes vamos falar sobre logs.

2.1.11. Implementar os logs do Hibernate

É possível conhecer os comandos SQL emitidos no banco de dados pela camada JPA / Hibernate. É interessante conhecê-los para verificar se a camada JPA é tão eficiente quanto um desenvolvedor que tivesse escrito ele mesmo as ordens SQL.

Com o JPA / Hibernate, os logs SQL podem ser verificados no arquivo [persistence.xml]:


            <!-- Classes persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexão JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />

  • linhas 4-6: os logs SQL ainda não estavam ativados. Agora, eles são ativados removendo a tag de comentário das linhas 3 e 7.

Reexecuta-se o aplicativo [InitDB]. As mensagens no console passam a ser as seguintes:

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 ordem SQL delete resultante da instrução:

        // excluir itens da tabela de pessoas
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
  • linhas 5-18: as ordens SQL insert decorrentes das instruções:

        // persistência de pessoas
        em.persist(p1);
        em.persist(p2);
  • linhas 21-32: o comando `SQL select` proveniente da instrução:

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

Se fizermos exibições intermediárias no console, veremos que a gravação dos logs SQL de uma instrução I do código Java ocorre quando a instrução I é executada. Isso não significa que a ordem SQL exibida seja executada no banco de dados nesse momento. Na verdade, ela é armazenada em cache para ser executada na próxima sincronização do contexto de persistência com o banco de dados.

Outros registros podem ser obtidos por meio do arquivo [src/log4j.properties]:

  • no [1], o arquivo [log4j.properties] é processado pelo arquivo [log4j-1.2.13.jar] [2] da ferramenta denominada LOG4j (Logs for Java), disponível no endereço [http://logging.apache.org/log4j/docs/index.html]. Localizado na pasta [src] do projeto Eclipse, sabemos que o [log4j.properties] será copiado automaticamente para a pasta [bin] do projeto [3]. Feito isso, ele agora está na pasta classpath do projeto, e é de lá que o arquivo [2] irá buscá-lo.

O arquivo [log4j.properties] nos permite monitorar alguns logs do Hibernate. Nas execuções anteriores, seu conteúdo era o seguinte:


# Encaminhar mensagens de log para 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

# Opção de registro do root
log4j.rootLogger=ERROR, stdout

# Opções de registro do Hibernate (INFO exibe apenas mensagens de inicialização)
#log4j.logger.org.hibernate=INFO

# Registrar argumentos de tempo de execução dos parâmetros de ligação do JDBC
#log4j.logger.org.hibernate.type=DEBUG

Não vou comentar muito sobre essa configuração, pois nunca dediquei tempo para me informar a fundo sobre o LOG4j.

  • As linhas 1 a 8 estão presentes em todos os arquivos log4j.properties que encontrei
  • as linhas 10 a 14 estão presentes nos arquivos log4j.properties dos exemplos do Hibernate.
  • Linha 11: controla os logs gerais do Hibernate. Como a linha está comentada, esses logs estão desativados aqui. É possível ter vários níveis de logs: INFO (informações gerais sobre o que o Hibernate está fazendo), WARN (o Hibernate nos avisa sobre um possível problema), DEBUG (logs detalhados). O nível INFO é o menos detalhado, enquanto o modo DEBUG é o mais detalhado. Ativar a linha 11 permite saber o que o Hibernate está fazendo, especialmente no início da aplicação. Isso costuma ser interessante.
  • A linha 12, se estiver ativa, permite saber quais argumentos foram efetivamente utilizados durante a execução das consultas SQL configuradas.

Vamos começar removendo o comentário da linha 14


# Registro de argumentos de tempo de execução dos parâmetros de ligação do JDBC
log4j.logger.org.hibernate.type=DEBUG

e reexecutemos [InitDB]. Os novos registros gerados por essa modificação são os seguintes (visã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 a 10 são novos registros gerados pela ativação da linha 14 do [log4j.properties]. Elas indicam os 5 valores atribuídos aos parâmetros formais ? da consulta parametrizada das linhas 2 a 7. Assim, vemos que a coluna VERSION receberá o valor 0 (linha 8).

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

# Opções de registro do Hibernate (INFO exibe apenas mensagens de inicialização)
log4j.logger.org.hibernate=INFO

e executemos novamente o [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 análise desses logs traz muitas informações interessantes:

  • linha 7: o Hibernate indica o nome de uma classe @Entity que encontrou
  • linha 8: indica que a classe [Personne] será associada à tabela [jpa01_personne]
  • linha 9: indica o pool de conexões C3P0 que será utilizado, o nome do driver JDBC e a URL do banco de dados a ser gerenciado
  • linha 10: fornece outras características da conexão JDBC: proprietário, tipo de commit, etc.
  • linha 14: o dialeto utilizado para se comunicar com o SGBD
  • linha 15: o tipo de transação utilizado. JDBCTransactionFactory indica que a aplicação gerencia suas próprias transações. Ela não é executada em um contêiner EJB3 que forneceria seu próprio serviço de transações.
  • As linhas seguintes referem-se a opções de configuração do Hibernate com as quais não nos deparamos. O leitor interessado é convidado a consultar a documentação do Hibernate.
  • linha 37: os comandos SQL serão exibidos no console. Isso 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 do banco de dados é exportado para os arquivos SGBD e c.a.d. O banco de dados é esvaziado e, em seguida, recriado. Esse mecanismo decorre da configuração definida no [persistence.xml] (linha 4 abaixo):

            ...
            <property name="hibernate.connection.password" value="jpa" />
            <!-- criação automática do esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialeto -->
            ...

Quando um aplicativo “trava” com uma exceção do Hibernate que não se consegue entender, deve-se começar ativando os logs do Hibernate no modo DEBUG no [log4j.properties] para obter mais clareza:


# Opção de logger raiz
log4j.rootLogger=ERROR, stdout

# Opções de registro do Hibernate (o INFO exibe apenas mensagens de inicialização)
log4j.logger.org.hibernate=DEBUG

No restante deste documento, os logs estão desativados por padrão para que a exibição no console seja mais legível.

2.1.12. Conheça a linguagem JPQL / HQL com o console do Hibernate

Observação: esta seção requer o plug-in Hibernate Tools (parágrafo 5.2.5).

No código do aplicativo [InitDB], utilizamos uma consulta JPQL. JPQL (Java Persistence Query Language) é uma linguagem para consultar o contexto de persistência. A consulta encontrada foi a seguinte:

select p from Personne p order by p.nom asc

Ela selecionava todos os elementos da tabela associada à @Entity [Personne] e os retornava em ordem crescente pelo nome. Na consulta acima, p.nom é o campo “nome” de uma instância p da classe [Personne]. Portanto, uma consulta JPQL opera sobre os objetos @Entity do contexto de persistência e não diretamente sobre as tabelas do banco de dados. A camada JPA, por sua vez, traduzirá essa consulta JPQL em uma consulta SQL adequada ao SGBD com o qual ela trabalha. Assim, no caso de uma implementação JPA / Hibernate ligada a um SGBD MySQL5, a consulta JPQL anterior é convertida na consulta SQL seguinte:

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 [Personne] para gerar a ordem SQL correta. Trata-se da ponte objeto/relacional que foi implementada aqui.

O plug-in [Hibernate Tools] (parágrafo 5.2.5) oferece uma ferramenta chamada “Console Hibernate” que permite

  • emitir ordens JPQL ou do superconjunto HQL (Hibernate Query Language) no contexto de persistência
  • obter os resultados
  • conhecer o equivalente SQL que foi executado no banco de dados

O console do Hibernate é uma ferramenta de grande valor para aprender a linguagem JPQL e se familiarizar com a ponte JPQL / SQL. Sabe-se que o JPA se inspirou fortemente em ferramentas ORM, como o Hibernate ou o Toplink. O JPQL é muito semelhante à linguagem HQL do Hibernate, mas não inclui todas as suas funcionalidades. No console do Hibernate, é possível emitir comandos HQL que serão executados normalmente no console, mas que não fazem parte da linguagem JPQL e, portanto, não poderiam ser utilizados em um cliente JPA. Quando for esse o caso, iremos sinalizar.

Vamos criar um console do Hibernate para nosso projeto atual do Eclipse:

  • [1]: mudamos para uma perspectiva [Hibernate Console] (Janela / Abrir Perspectiva / 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 será criada. Seu nome é exibido em [3].
  • Em [5], atribuímos o nome desejado a essa configuração. Aqui, utilizamos [3].
  • No [6], indicamos que estamos usando uma configuração JPA para que a ferramenta saiba que deve utilizar o arquivo [META-INF/persistence.xml]
  • em [7]: indicamos que, nesse arquivo [META-INF/persistence.xml], deve-se utilizar a unidade de persistência chamada jpa.
  • No [8], validamos a configuração.

Em seguida, é necessário que o SGBD seja executado. Aqui, trata-se do MySQL5.

  • no [1]: a configuração criada apresenta uma árvore com três ramos
  • em [2]: o ramo [Configuration] lista os objetos que o console utilizou para se configurar: neste caso, a @Entity Personne.
  • em [3]: a Session Factory é um conceito do Hibernate semelhante ao EntityManager de JPA. Ela realiza a ponte entre objetos e relações por meio dos objetos do ramo [Configuration]. Em [3] são apresentados os objetos do contexto de persistência, aqui novamente a @Entity Personne.
  • Em [4]: o banco de dados acessado por meio da configuração encontrada em [persistence.xml]. Nele, encontramos a tabela [jpa01_personne].
  • em [1], cria-se um editor HQL
  • no editor HQL,
    • no [2], escolhe-se a configuração do Hibernate a ser usada, caso haja mais de uma
    • no [3], digita-se o comando JPQL que se deseja executar
    • em [4], execute-o
  • em [5], obtêm-se os resultados da consulta na janela [Hibernate Query Result]. Podem surgir duas dificuldades aqui:
    • não se obtém nada (nenhuma linha). O console do Hibernate utilizou o conteúdo de [persistence.xml] para criar uma conexão com o SGBD. No entanto, essa configuração possui uma propriedade que determina o esvaziamento do banco de dados:

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

Portanto, é necessário reexecutar o aplicativo [InitDB] antes de executar novamente o comando JPQL acima.

  • (continuação)
    • Não aparece a janela [Hibernate Query Result]. Ela é solicitada por meio do comando [Window / Show View / ...]

A janela [Hibernate Dynamic SQL preview] ([1] abaixo) permite visualizar a consulta SQL que será executada para processar o comando JPQL que estamos digitando. Assim que a sintaxe do comando JPQL estiver correta, o comando SQL correspondente aparece nesta janela:

  • em [2], apaga-se o comando anterior HQL
  • em [3], executa-se um novo comando
  • em [4], o resultado
  • em [5], o comando SQL que foi executado com base

O editor HQL oferece ajuda para a criação de comandos HQL:

  • em [1]: assim que o editor reconhece que p é um objeto Personne, ele pode sugerir os campos de p durante a digitação.
  • em [2]: um comando HQL incorreto. É preciso escrever where p.marie=true.
  • em [3]: o erro é sinalizado na janela [SQL Preview]

Convidamos o leitor a executar outros comandos HQL / JPQL na base.

2.1.13. Um segundo cliente JPA

Voltemos à perspectiva Java do projeto:

 
  • [InitDB.java] é um programa que inseria algumas linhas na tabela [jpa01_personne] do banco de dados. A análise de seu código nos permitiu obter os primeiros elementos do API e do JPA.
  • [Main.java] é um programa que realiza as operações CRUD na tabela [jpa01_personne]. A análise de seu código nos permitirá revisar os conceitos fundamentais do contexto de persistência e do ciclo de vida dos objetos desse contexto.

2.1.13.1. A estrutura do código

O [Main.java] executará uma série de testes, cada um com o objetivo de demonstrar uma faceta específica do JPA:

 

O método [main]

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

Apresentamos o método main e os métodos denominados utilitários:


package tests;

...
import entites.Personne;

@SuppressWarnings("unchecked")
public class Main {

    // constantes
    private final static String TABLE_NAME = "jpa01_personne";

    // Contexto de persistência
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
    private static EntityManager em = null;

    // objetos compartilhados
    private static Personne p1, p2, newp1;

    public static void main(String[] args) throws Exception {
        // Limpeza do banco de dados
        log("clean");clean();

        // dump de tabela
        dump();

        // teste1
        log("test1");test1();

...
        // teste11
        log("test11");test11();

        // fim do contexto de persistência
        if (em.isOpen())
            em.close();

        // fechamento de EntityManagerFactory
        emf.close();
    }

    // recuperar o EntityManager atual
    private static EntityManager getEntityManager() {
        if (em == null || !em.isOpen()) {
            em = emf.createEntityManager();
        }
        return em;
    }

    // obter um novo EntityManager
    private static EntityManager getNewEntityManager() {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = emf.createEntityManager();
        return em;
    }

    // exibir o conteúdo da tabela
    private static void dump() {
        // contexto de persistência atual
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // exibição de pessoas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fim da transação
        tx.commit();
    }

    // limpar BD
    private static void clean() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // excluir os elementos da tabela PERSONNES
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // fim da transação
        tx.commit();
    }

    // registros
    private static void log(String message) {
        System.out.println("main : ----------- " + message);
    }

    // criação de objetos
    public static void test1() throws ParseException {
...
    }

    // modificar um objeto do contexto
    public static void test2() {
...
    }

    // solicitar objetos
    public static void test3() {
...
    }

    // excluir um objeto pertencente ao contexto de persistência
    public static void test4() {
....
    }

    // desvincular, vincular novamente e modificar
    public static void test5() {
...
    }

    // excluir um objeto que não pertença ao contexto de persistência
    public static void test6() {
...
    }

    // modificar um objeto que não pertence ao contexto de persistência
    public static void test7() {
...
    }

    // reanexar um objeto ao contexto de persistência
    public static void test8() {
...
    }

    // uma consulta SELECT provoca uma sincronização
    // do banco de dados com o contexto de persistência
    public static void test9() {
....
    }

    // controle de versão (bloqueio otimista)
    public static void test10() {
...
    }

    // reversão de uma transação
    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]. Ele nos permitirá criar, ao longo da aplicação, diversos contextos de persistência.
  • linha 14: um contexto de persistência EntityManager ainda não inicializado
  • linha 17: três objetos [Personne] compartilhados pelos testes
  • linha 21: a tabela jpa01_personne é esvaziada e, em seguida, exibida na linha 24 para garantir que partimos de uma tabela vazia.
  • linhas 27-31: sequência de testes
  • linhas 34-35: fechamento do contexto de persistência, caso estivesse aberto.
  • linha 38: fechamento do objeto EntityManagerFactory emf.
  • linhas 42-47: o método [getEntityManager] torna o EntityManager (ou contexto de persistência) atual ou cria um novo caso de ele não exista (linhas 43-44).
  • linhas 50-56: o método [getNewEntityManager] cria um novo contexto de persistência. Se já existisse um anteriormente, ele é fechado (linhas 51-52)
  • linhas 59-72: o método [dump] exibe o conteúdo da tabela [jpa01_personne]. Esse código já foi encontrado em [InitDB].
  • linhas 75-85: o método [clean] esvazia a tabela [jpa01_personne]. Esse código já foi encontrado em [InitDB].
  • linhas 88-90: o método [log] exibe no console a mensagem que lhe é passada como parâmetro, para que ela seja notada.

Agora podemos passar à análise dos testes.

2.1.13.2. Teste 1

O código do teste 1 é o seguinte:


// criação de objetos
    public static void test1() throws ParseException {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // criação de pessoas
        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);
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistência de pessoas
        em.persist(p1);
        em.persist(p2);
        // fim da transação
        tx.commit();
        // exibição da tabela
        dump();

}

Esse código já foi encontrado em [InitDB]: ele cria duas pessoas e as insere no contexto de persistência.

  • linha 4: solicita-se o contexto de persistência atual
  • linhas 6-7: criam-se as duas pessoas
  • linhas 9-15: os dois usuários são colocados no contexto de persistência dentro de uma transação.
  • linha 15: devido ao commit da transação, ocorre a sincronização do contexto de persistência com o banco de dados. As duas pessoas serão adicionadas à tabela [jpa01_personne].
  • linha 17: exibe-se a tabela

A exibição no console deste 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 do teste 2 é o seguinte:


// alterar um objeto do contexto
    public static void test2() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementa o número de filhos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // altera-se seu estado civil
        p1.setMarie(false);
        // o objeto p1 é salvo automaticamente (verificação de alterações)
        // na próxima sincronização (commit ou select)
        // fim da transação
        tx.commit();
        // exibe-se a nova tabela
        dump();
    }
  • O objetivo do teste 2 é modificar um objeto do contexto de persistência e, em seguida, exibir o conteúdo da tabela para verificar se a modificação ocorreu
  • linha 4: recuperamos o contexto de persistência atual
  • linhas 6-7: as operações serão realizadas em uma transação
  • linhas 9 e 11: o número de filhos da pessoa p1 é alterado, assim como seu estado civil
  • linha 15: fim da transação, portanto, sincronização do contexto de persistência com o banco de dados
  • linha 17: exibição da tabela

A saída na console do 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: a pessoa p1 antes da modificação
  • linha 8: a pessoa p1 após a modificação. Observe-se que seu número de versão passou para 1. Esse número é incrementado em 1 a cada atualização da linha.

2.1.13.4. Teste 3

O código do teste 3 é o seguinte:


    // solicitar objetos
    public static void test3() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicita-se a pessoa p1
        Personne p1b = em.find(Personne.class, p1.getId());
        // como p1 já está no contexto de persistência, não houve acesso ao banco de dados
        // p1b e p1 são as mesmas referências
        System.out.format("p1==p1b ? %s%n", p1 == p1b);
        // solicitar um objeto que não existe retorna um ponteiro nulo
        Personne px = em.find(Personne.class, -4);
        System.out.format("px==null ? %s%n", px == null);
        // fim da transação
        tx.commit();
}
  • O teste 3 se concentra no método [EntityManager.find], que permite buscar um objeto no banco de dados para colocá-lo no contexto de persistência. A partir de agora, não explicaremos mais a transação que ocorre em todos os testes, exceto quando ela for utilizada de forma incomum.
  • linha 9: solicita-se ao contexto de persistência a pessoa que possui a mesma chave primária que a pessoa p1. Há dois casos:
    • p1 já se encontra no contexto de persistência. É o que ocorre neste caso. Portanto, não é feito nenhum acesso ao banco de dados. O método find limita-se a retornar uma referência ao objeto persistido.
    • p1 não está no contexto de persistência. Portanto, é feito um acesso ao banco de dados, por meio da chave primária fornecida. A linha recuperada é colocada no contexto de persistência e find retorna a referência desse novo objeto persistido.
  • linha 12: verifica-se se find já retornou a referência do objeto p1 no contexto
  • linha 14: solicita-se um objeto que não existe nem no contexto de persistência nem no banco de dados. O método find retorna, então, o ponteiro null. Esse ponto é verificado na linha 15.

A saída na console do teste 3 é a seguinte:

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

2.1.13.5. Teste 4

O código do teste 4 é o seguinte:


    // excluir um objeto pertencente ao contexto de persistência
    public static void test4() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // o objeto persistido p2 é excluído
        em.remove(p2);
        // fim da transação
        tx.commit();
        // exibindo a nova tabela
        dump();
}
  • O teste 4 analisa o método [EntityManager.remove], que permite excluir um elemento do contexto de persistência e, consequentemente, do banco de dados.
  • linha 9: a pessoa p2 é removida do contexto de persistência
  • linha 11: sincronização do contexto com o banco de dados
  • linha 13: exibição da tabela. Normalmente, a pessoa p2 não deveria mais estar lá.

A exibição na console do 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: a pessoa p2 em test1
  • linhas 12-14: ela não existe mais após a execução de test4.

2.1.13.6. Teste 5

O código do teste 5 é o seguinte:


// desvincular, vincular novamente e modificar
    public static void test5() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 desanexado
        Personne oldp1=p1;
        // reanexando p1 ao novo contexto
        p1 = em.find(Personne.class, p1.getId());
        // verificação
        System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);        
        // fim da transação
        tx.commit();
        // incrementa-se o número de filhos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // exibe-se a nova tabela
        dump();
    }
  • O teste 5 analisa o ciclo de vida dos objetos persistentes ao longo de vários contextos de persistência sucessivos. Até agora, sempre utilizamos o mesmo contexto de persistência nos diferentes testes.
  • linha 4: um novo contexto de persistência é solicitado. O método [getNewEntityManager] fecha o anterior e abre um novo. Isso faz com que os objetos p1 e p2 mantidos pela aplicação não estejam mais em um estado persistente. Eles pertenciam a um contexto que foi fechado. Diz-se que eles estão em um estado desanexado. Eles não pertencem ao novo contexto de persistência.
  • linhas 6-7: início da transação. Ela será utilizada aqui de maneira incomum.
  • linha 9: registra-se o endereço do objeto p1, agora desanexado.
  • linha 11: solicita-se ao contexto de persistência a pessoa p1 (com a chave primária de p1). Como o contexto é novo, a pessoa p1 não está nele. Portanto, ocorrerá um acesso ao banco de dados. O objeto retornado será inserido no novo contexto.
  • linha 13: verifica-se se o objeto persistente p1 do contexto é diferente do objeto oldp1, que era o antigo objeto p1 desanexado.
  • linha 15: a transação é encerrada
  • linha 17: modifica-se, fora da transação, o novo objeto persistido p1. O que acontece nesse caso? Queremos saber.
  • linha 19: solicita-se a exibição da tabela. Vale lembrar que, devido ao select emitido pelo método dump, é realizada automaticamente uma sincronização do contexto de persistência com o banco de dados.

A exibição na console do 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 efetivamente acessou o banco 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, feita fora da transação, foi, portanto, considerada. Isso, na verdade, depende do SGBD utilizado. Em um SGBD, uma ordem SQL é sempre executada dentro de uma transação. Se o cliente JPA não iniciar por conta própria uma transação explícita, o SGBD iniciará, então, uma transação implícita. Há dois casos comuns:
    • 1 - cada ordem SQL individual é objeto de uma transação, aberta antes da ordem e fechada depois. Diz-se que estamos no modo autocommit. Tudo ocorre, portanto, como se o cliente JPA realizasse transações para cada ordem SQL.
    • 2 - o SGBD não está no modo autocommit e inicia uma transação implícita na primeira ordem SQL, que o cliente JPA emite fora de uma transação, deixando que o próprio cliente a feche. Todas as ordens SQL emitidas pelo cliente JPA passam, então, a fazer parte da transação implícita. Essa transação pode ser encerrada por diversos eventos: o cliente encerra a conexão, inicia uma nova transação, etc.

Estamos em uma situação que depende da configuração do SGBD. Portanto, temos um código não portátil. Mostraremos, um pouco mais adiante, um código sem transações e veremos que nem todos os SGBD apresentam o mesmo comportamento em relação a esse código. Consideraremos, portanto, que trabalhar fora de transações é um erro de programação.

  • linha 7: observe-se que o número da versão passou para 2.

2.1.13.7. Teste 6

O código do teste 6 é o seguinte:


// excluir um objeto que não pertence ao contexto de persistência
    public static void test6() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // elimina-se p1, que não pertence ao novo contexto
        try {
            em.remove(p1);
            // fim da transação
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
            // é feito um rollback da transação
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // exibindo a nova tabela
        dump();
    }
  • O teste 6 busca excluir um objeto que não pertence ao contexto de persistência.
  • linha 4: é solicitado um novo contexto de persistência. O antigo é, portanto, fechado e os objetos que ele continha ficam desassociados. É o caso do objeto p1 do teste 5 anterior.
  • linhas 6-7: início da transação.
  • linha 10: o objeto desanexado p1 é excluído. Sabemos que isso causará uma exceção, por isso colocamos a operação dentro de um 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 da transação são validadas) ou um rollback (todas as operações da transação são canceladas). Ocorreu uma exceção, portanto, executamos um rollback na transação. Não há nada a reverter, já que a única operação da transação falhou, mas o rollback encerra a transação. É a primeira vez que usamos a operação [EntityTransaction].rollback. Deveríamos ter feito isso 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 do rollback da transação deve sempre ser previsto no código.
  • linha 24: exibimos a tabela. Normalmente, ela não deve ter mudado.

A exibição na console do 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: a exclusão de p1 falhou. A mensagem de exceção explica que tentamos excluir um objeto destacado, portanto, que não faz parte do contexto. Isso não é possível.
  • linha 8: a pessoa p1 ainda está presente.

2.1.13.8. Teste 7

O código do teste 7 é o seguinte:


// modificar um objeto que não pertence ao contexto de persistência
    public static void test7() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementa-se o número de filhos de p1 que não pertencem ao novo contexto
        p1.setNbenfants(p1.getNbenfants() + 1);
        // fim da transação
        tx.commit();
        // exibindo a nova tabela — ela não deve ter mudado
        dump();
    }
  • O teste 7 busca modificar um objeto que não pertence ao contexto de persistência e verificar o impacto que isso tem no banco de dados. É possível imaginar que não haja impacto algum. É o que mostram os resultados do teste.
  • linha 4: é solicitado um novo contexto de persistência. Portanto, temos um contexto novo, sem objetos persistentes nele.
  • linhas 6-7: início da transação.
  • linha 9: modifica-se o objeto desanexado p1. Trata-se de uma operação que não envolve o contexto de persistência em. Portanto, não se deve esperar uma exceção ou algo do tipo. É uma operação básica em um POJO.
  • linha 11: o commit provoca a sincronização do contexto com o banco de dados. Esse contexto está vazio. Portanto, o banco de dados não é alterado.
  • linha 24: exibimos a tabela. Normalmente, ela não deve ter mudado.

A exibição no console do 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 no banco de dados. Para o próximo teste, vale lembrar, no entanto, que, na memória, o número de filhos dela agora é 5.

2.1.13.9. Teste 8

O código do teste 8 é o seguinte:


    // reassociar um objeto ao contexto de persistência
    public static void test8() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // reanexamos o objeto p1, que estava desanexado, ao novo contexto
        newp1 = em.merge(p1);
        // agora é o newp1 que faz parte do contexto, e não o p1
        // fim da transação
        tx.commit();
        // exibe-se a nova tabela — o número de filhos de p1 deve ter mudado
        dump();
}
  • O teste 8 reconecta um objeto desconectado ao contexto de persistência.
  • linha 4: é solicitado um novo contexto de persistência. Portanto, temos um contexto novo sem objetos persistentes nele.
  • linhas 6-7: início da transação.
  • linha 9: o objeto desanexado p1 é reanexado ao contexto de persistência. A operação merge pode envolver várias operações:
    • caso 1: existe no contexto de persistência um objeto persistente ps1 com a mesma chave primária que o objeto desanexado p1. O conteúdo de p1 é copiado para ps1, e merge passa a referenciar ps1.
    • Caso 2: não existe no contexto de persistência um objeto persistente ps1 com a mesma chave primária que o objeto desanexado p1. A base de dados é então consultada para verificar se o objeto procurado existe nela. Se sim, esse 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, nem no contexto de persistência nem no banco de dados, um objeto com a mesma chave primária que o objeto desanexado p1. Um novo objeto [Personne] (new) é então criado e, em seguida, inserido no contexto de persistência. Em seguida, retorna-se ao caso 1.
    • Conclusão: o objeto desanexado p1 permanece desanexado. A operação merge retorna uma referência (neste caso, newp1) ao objeto persistente ps1, derivado do merge. O aplicativo cliente deve agora trabalhar com o objeto persistente ps1 e não com o objeto desanexado p1.
    • Observe-se uma diferença entre os casos 1 e 3 quanto à ordem SQL programada para o merge: nos casos 1 e 2, trata-se da ordem UPDATE, enquanto no caso 3, trata-se de uma ordem INSERT.
  • linha 12: o commit provoca a sincronização do contexto com o banco de dados. Esse contexto não está mais vazio. Ele contém o objeto newp1. Este será persistido no banco de dados.
  • linha 24: exibimos a tabela para verificar.

A exibição na console do 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 passou para 5 no teste 7, mas não foi gravado no banco de dados (linha 7). Após o merge, o newp1 foi gravado no banco de dados: na linha 10, há efetivamente 5 filhos.
  • linha 10: o número da versão de newp1 passou para 3.

2.1.13.10. Teste 9

O código do teste 9 é o seguinte:


// uma consulta SELECT provoca uma sincronização
    // do banco de dados com o contexto de persistência
    public static void test9() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementa-se o número de filhos de newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // exibição de pessoas — o número de filhos de newp1 deve ter mudado
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fim da transação
        tx.commit();
    }
  • O teste 9 visa demonstrar o mecanismo de sincronização do contexto que ocorre automaticamente antes de um select.
  • linha 5: não se altera o contexto de persistência. O newp1, portanto, está 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: exibe-se a tabela por meio de um SELECT. O contexto será sincronizado com o banco de dados antes da execução do select.
  • linha 17: fim da transação

Para visualizar a sincronização, ativamos a exibição dos logs do Hibernate no modo DEBUG (log4j.properties):


# Opção do registrador raiz
log4j.rootLogger=ERROR, stdout

# Opções de registro do Hibernate (INFO exibe apenas mensagens de inicialização)
log4j.logger.org.hibernate=DEBUG

A exibição no console do 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: o teste 9 é iniciado
  • linhas 2-6: a transação JDBC é iniciada. O modo autocommit do SGBD está desativado (linha 5)
  • linha 7: exibição provocada pela linha 12 do código Java. As linhas seguintes do código Java provocarão um select e, portanto, uma sincronização do contexto de persistência com o banco de dados.
  • linha 8: a ordem JPQL que se deseja emitir já foi emitida. O Hibernate a encontra em seu cache de “consultas preparadas”.
  • linha 9: o Hibernate anuncia que irá realizar um flush do contexto de persistência
  • linhas 11-12: o Hibernate (Hb) detecta que a entidade Pessoa#1 (com chave primária 1) foi alterada (dirty).
  • linhas 12-13: O Hb anuncia que atualiza esse elemento e altera 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 (update) e 0 exclusões (delete)
  • linhas 17-34: sincronização do contexto (flush). Observações: o incremento da versão (linha 19), a ordem SQL update preparada (linha 21), os valores dos parâmetros da ordem update (linhas 24-31).
  • linha 35: o select é iniciado
  • linha 38: a ordem SQL que será executada
  • linha 40: o select retorna apenas uma linha
  • linha 42: o Hb descobre que já possui, em seu contexto de persistência, a entidade Personne#1 que a consulta SELECT retornou do banco de dados. Portanto, ele não copia a linha obtida do banco de dados para o contexto, operação que ele denomina “hidratação”.
  • linha 43: ele verifica se os objetos retornados pelo select possuem dependências (geralmente chaves estrangeiras) que também precisariam ser carregadas (coleções não preguiçosas). Neste caso, 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 a execução de commit.
  • linha 48: o Hb constata que o contexto não sofreu alterações desde a sincronização anterior.
  • linha 50: fim do commit.

Mais uma vez, os logs do Hibernate no modo DEBUG se mostram muito úteis para saber exatamente o que o Hibernate está fazendo.

2.1.13.11. Teste 10

O código do teste 10 é o seguinte:


// controle de versão (bloqueio otimista)
    public static void test10() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementar a versão de newp1 diretamente no banco de dados (consulta nativa)
        em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
        // fim da transação
        tx.commit();
        // início de uma nova transação
        tx = em.getTransaction();
        tx.begin();
        // incrementamos o número de filhos de newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // fim da transação — ela deve falhar, pois newp1 não está mais na versão correta
        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());
            // realiza-se um rollback da transação
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // fecha-se o contexto, que não está mais atualizado
        em.close();
        // dump da tabela — a versão de p1 deve ter mudado
        dump();
    }
  • O teste 10 visa demonstrar o mecanismo proporcionado pelo campo version da @Entity Pessoa, que possui o atributo JPA @Version. Já explicamos que essa anotação faz com que, no banco de dados, o valor da coluna associada à anotação @Version seja incrementado a cada update realizado na linha à qual ela pertence. Esse mecanismo, também chamado de bloqueio otimista (optimistic locking), exige que o cliente que deseja modificar um objeto O no banco de dados possua a versão mais recente do mesmo. Se ele não a possuir, significa que o objeto foi modificado desde que ele o obteve, e ele deve ser avisado disso.
  • linha 4: não se altera o contexto de persistência. newp1, portanto, está 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 no banco de dados. As consultas do tipo nativeQuery ignoram o contexto de persistência e acessam diretamente o banco de dados. O resultado é que o objeto persistente newp1 e sua representação no banco de dados não têm mais 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. Portanto, ocorre uma sincronização. Ela provocará a atualização do número de filhos de newp1 no banco de dados. Essa atualização falhará porque o objeto persistente newp1 está na versão 4, enquanto que no banco de dados o objeto a ser atualizado está na versão 5. Será lançada uma exceção, o que justifica o uso de try/catch no código.
  • linha 21: exibe-se a exceção e sua causa.
  • linha 25: reversão da transação
  • linha 33: exibição da tabela: deve-se verificar que a versão de newp1 é 5 no banco de dados.

A exibição no console do 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 realmente lança uma exceção. Ela é do tipo [javax.persistence.RollbackException]. A mensagem associada é vaga. Se analisarmos a causa dessa exceção (Exception.getCause), vemos que se trata de uma exceção do Hibernate devido ao fato de estarmos tentando modificar uma linha do banco de dados sem ter a versão correta.
  • linha 7: vemos que a versão de newp1 no banco de dados foi, de fato, alterada para 5 pela nativeQuery.

2.1.13.12. Teste 11

O código do teste 11 é o seguinte:


// reversão de uma transação
    public static void test11() throws ParseException {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = null;
        try {
            tx = em.getTransaction();
            tx.begin();
            // reassocia-se p1 ao contexto, buscando-o no banco de dados
            p1 = em.find(Personne.class, p1.getId());
            // incrementa-se o número de filhos de p1
            p1.setNbenfants(p1.getNbenfants() + 1);
            // exibição de pessoas — o número de filhos de p1 deve ter mudado
            System.out.println("[personnes]");
            for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
                System.out.println(p);
            }
            // criação de duas pessoas com o mesmo nome, o que é proibido pela 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);
            // persistência das pessoas
            em.persist(p3);
            em.persist(p4);
            // fim da transação
            tx.commit();
        } catch (RuntimeException e1) {
            // ocorreu um problema
            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());
            }
            // estamos abandonando o contexto atual
            em.clear();
        }
        // dump — a tabela não deve ter sido alterada devido ao rollback
        dump();
    }
  • O teste 11 analisa o mecanismo da transação rollback. Uma transação funciona no princípio do “tudo ou nada”: as operações SQL que ela contém são todas executadas com sucesso (commit) ou todas revertidas em caso de falha de uma delas (rollback).
  • linha 4: continuamos com o mesmo contexto de persistência. O leitor talvez se lembre de que o contexto foi fechado após a falha do teste anterior. Nesse caso, o [getEntityManager] fornece um contexto totalmente novo, portanto vazio.
  • linhas 7-27: um único try/catch para controlar os problemas que poderemos encontrar
  • linhas 8-9: início de uma transação que conterá várias operações SQL
  • linha 11: p1 é buscado no banco de dados e inserido no contexto
  • linha 13: aumenta-se o número de filhos de p1 (6 → 7)
  • linhas 15-18: exibe-se o conteúdo do banco de dados, o que forçará uma sincronização do contexto. No banco de dados, o número de filhos de p1 passará para 7, o que deve ser confirmado pela exibição no console.
  • linhas 20-21: criação de duas pessoas, p3 e p4, com o mesmo nome. No entanto, o campo “nome” da @Entity Pessoa possui o atributo unique=true, o que resultou na criação de uma restrição de exclusividade na coluna NOM da tabela [jpa01_personne].
  • linhas 23-24: as pessoas p3 e p4 são inseridas no contexto de persistência.
  • linha 26: a transação é confirmada. Segue-se uma segunda sincronização do contexto, tendo a primeira ocorrido por ocasião do select. JPA emitirá duas ordens SQL e insert para as pessoas p3 e p4. p3 será inserido. Para p4, o SGBD lançará uma exceção, pois p4 tem o mesmo nome que p3. Portanto, o p4 não é inserido e o driver JDBC retorna uma exceção ao cliente.
  • linha 27: tratamos a exceção
  • linhas 29-31: exibimos a exceção e suas duas causas anteriores na cadeia de exceções que nos trouxe até aqui.
  • linha 34: faz-se um rollback da transação atualmente ativa. Essa transação começou na linha 9 do código Java. Desde então, foi realizada uma operação update para alterar o número de filhos de p1 e, em seguida, uma operação insert para a pessoa p3. Tudo isso será anulado pelo rollback.
  • linha 39: o contexto de persistência é esvaziado
  • linha 42: a tabela [jpa01_personne] é exibida. É preciso verificar se p1 ainda tem 6 filhos e se nem p3 nem p4 constam na tabela.

A exibição na console do 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 passou de 6 para 7 no banco de dados; a versão de p1 passou para 6.
  • linha 4: a exceção capturada durante o commit da transação. Se lermos com atenção, vemos que a causa é uma chave duplicada X (o nome). É a inserção de p4 que provoca esse erro, já que p3, já inserido, também tem o nome X.
  • linha 7: a tabela após o rollback. p1 voltou à versão 5 e ao número de filhos 6; p3 e p4 não foram inseridos.

2.1.13.13. Teste 12

O código do teste 12 é o seguinte:


    // repetimos o mesmo procedimento, mas sem as transações
    // obtemos o mesmo resultado de antes com os SGBD: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
    //; com SQLSERVER, a tabela fica vazia. A conexão é deixada em um estado que impede a reexecução
    // do programa. É necessário, então, reiniciar o servidor.
    // o mesmo ocorre com o SGBD Derby
    // HSQL insere a primeira pessoa — não há reversão

    public static void test12() throws ParseException {
        // reconectamos p1
        p1 = em.find(Personne.class, p1.getId());
        // incrementa o número de filhos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // exibição de pessoas — o número de filhos de p1 deve ter mudado
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // criação de duas pessoas com o mesmo nome, o que é proibido pela 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);
        // persistência das pessoas
        em.persist(p3);
        em.persist(p4);
        // dump que provocará a sincronização do contexto em com o 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());
        }
        // fechamos o contexto atual
        em.close();
        // dump
        dump();
}
  • O teste 12 repete o mesmo procedimento do teste 11, mas fora da transação. Queremos ver o que acontece nesse caso.
  • linhas 1-6: apresentam os resultados dos testes com diversos SGBD:
  • com um certo número de SGBD (Firebird, Oracle, MySQL5, Postgres), obtemos o mesmo resultado que no teste 11. O que leva a crer que esses SGBD iniciaram por conta própria uma transação abrangendo todas as ordens SQL recebidas até aquela que causou o erro e que eles próprios iniciaram um rollback.
  • Com outros SGBD (SQL Server, Apache Derby), ocorre uma falha no aplicativo e/ou no SGBD.
  • Com o SGBD e o HSQLDB, parece que a transação aberta pelo SGBD está no modo autocommit: a modificação do número de filhos do p1 e a inserção do p3 são tornadas permanentes. Apenas a inserção do p4 falha.

Portanto, temos um resultado que depende do SGBD, o que torna o aplicativo não portátil. É importante lembrar que as operações no contexto de persistência devem sempre ser realizadas dentro de uma transação.

2.1.14. Alterar para SGBD

Voltemos à arquitetura de teste do nosso projeto atual:

A aplicação cliente [3] vê apenas a interface JPA [5]. Ela não vê nem a implementação real dessa interface, nem o SGBD de destino. Portanto, deve ser possível alterar esses dois elementos da cadeia sem alterações no cliente [3]. É isso que estamos tentando verificar agora, começando pela alteração do SGBD. Até agora, tínhamos utilizado o MySQL5. Apresentamos outros seis descritos nos anexos (parágrafo 5), na esperança de que, entre eles, esteja o SGBD, o favorito do leitor.

De qualquer forma, a alteração a ser feita no projeto do Eclipse é simples (veja abaixo): substituir o arquivo de configuração da camada JPA por um dos arquivos da pasta conf [2] do projeto. Os drivers JDBC e SGBD já estão presentes na biblioteca [jpa-divers], [3] e [4].

2.1.14.1. Oracle 10g Express

O Oracle 10g Express é apresentado nos Anexos, no parágrafo 5.7. O arquivo 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">
        <!-- provedor -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Classes persistentes -->
            <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"/>
            -->
            <!-- conexão 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" />
            <!--  criação automática do esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  propriedades 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>

Essa configuração é idêntica à realizada para o SGBD e o MySQL5, com as seguintes diferenças:

  • linhas 15-18, que configuram a conexão JDBC com o banco de dados
  • linha 22: que define o dialeto SQL a ser utilizado

Nos exemplos a seguir, especificaremos apenas as linhas que mudam. Para uma explicação da configuração, consulte o anexo dedicado ao SGBD utilizado. Nele é apresentado, a cada vez, um exemplo de uso da conexão JDBC, no contexto do plug-in [SQL Explorer]. Com as informações do apêndice, o leitor poderá repetir a operação de verificação do resultado da aplicação do [InitDB] realizada no parágrafo 2.1.10.2.

Procedemos conforme indicado no parágrafo acima mencionado:

  • executar o SGBD Oracle
  • colocar conf/oracle/persistence.xml em META-INF/persistence.xml
  • executar o aplicativo [InitDB]

Os seguintes resultados são exibidos no console:

A partir de agora, não apresentaremos mais essa captura de tela, que é sempre a mesma. Mais interessante é a perspectiva do SQL Explorer sobre a ligação entre o JDBC e o SGBD. Seguiremos o procedimento explicado no parágrafo 2.1.8.

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

Feito isso, o leitor é convidado a executar o aplicativo [Main] e, em seguida, encerrar o SGBD.

2.1.14.2. PostgreSQL 8.2

O PostgreSQL 8.2 é apresentado nos Anexos, no parágrafo 5.6. Seu arquivo 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">
...
            <!-- conexão 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" />
...
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • executar o SGBD PostgreSQL
  • colocar conf/postgres/persistence.xml em META-INF/persistence.xml
  • executar o aplicativo [InitDB]

A perspectiva do SQL Explorer da ligação entre o JDBC e o SGBD é a seguinte:

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

Feito isso, o leitor é convidado a executar o aplicativo [Main] e, em seguida, encerrar o SGBD

2.1.14.3. SQL Server Express 2005

O SQL Server Express 2005 é apresentado nos Anexos, no parágrafo 5.8, página 270. Seu arquivo 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">
...
            <!-- conexão 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" />
...
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • execute o SGBD SQL Server
  • coloque conf/sqlserver/persistence.xml em META-INF/persistence.xml
  • executar o aplicativo [InitDB]

A perspectiva do SQL Explorer da ligação entre JDBC e SGBD é a seguinte:

  • em [1]: a conexão com o servidor SQL
  • em [2]: a árvore de conexão após a execução de [InitDB]
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: seu conteúdo.

Feito isso, o leitor é convidado a executar o aplicativo [Main] e, em seguida, encerrar o SGBD

2.1.14.4. Firebird 2.0

O Firebird 2.0 é apresentado nos Anexos, no parágrafo 5.4. Seu arquivo 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">
...
            <!-- conexão 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" />
...
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • execute o SGBD do Firebird
  • colocar conf/firebird/persistence.xml em META-INF/persistence.xml
  • execute o aplicativo [InitDB]

A perspectiva do SQL Explorer sobre a ligação entre o JDBC e o SGBD é a seguinte:

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

Feito isso, o leitor deve executar o aplicativo [Main] e, em seguida, encerrar o SGBD.

2.1.14.5. Apache Derby

O Apache Derby é apresentado nos Anexos, no parágrafo 5.10. Seu arquivo 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">
...
            <!-- conexão 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" />
...
            <!-- Dialeto -->
...
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • execute o SGBD do Apache Derby
  • coloque conf/derby/persistence.xml em META-INF/persistence.xml
  • execute o aplicativo [InitDB]

A perspectiva do SQL Explorer sobre a ligação entre o JDBC e o SGBD é a seguinte:

  • em [1]: a conexão com o Apache Derby
  • em [2]: a árvore de conexão após a execução de [InitDB]. Observe-se a tabela [HIBERNATE_UNIQUE_KEY] criada pelo JPA / Hibernate para gerar automaticamente os valores sucessivos da chave primária ID. Já mencionamos que esse mecanismo costuma ser proprietário. Isso fica claro aqui. Graças ao JPA, o desenvolvedor não precisa se aprofundar nesses detalhes do SGBD.
  • em [3]: a estrutura da tabela [jpa01_personne]
  • em [4]: seu conteúdo.

Feito isso, o leitor é convidado a executar o aplicativo [Main] e, em seguida, encerrar o SGBD.

2.1.14.6. HSQLDB

O HSQLDB é apresentado nos Anexos, no parágrafo 5.9. Seu arquivo 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">
...
            <!-- conexão 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="" />
            -->
...
            <!-- Dialeto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
        </properties>
    </persistence-unit>
</persistence>

Para executar o [InitDB]:

  • execute o SGBD HSQL
  • colocar conf/hsql/persistence.xml em META-INF/persistence.xml
  • executar o aplicativo [InitDB]

A perspectiva SQL do Explorer para a ligação entre JDBC e SGBD é a seguinte:

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

Feito isso, o leitor deve executar o aplicativo [Main] e, em seguida, encerrar o SGBD.

2.1.15. Alterar a implementação JPA

Voltemos à arquitetura de teste do nosso projeto atual:

O estudo anterior mostrou que conseguimos substituir o SGBD pelo [7] sem alterar nada no código do cliente [3]. Agora, alteramos a implementação JPA [6] e demonstramos, mais uma vez, que isso ocorre de forma transparente para o código cliente [3]. Vamos considerar uma implementação TopLink e [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:

2.1.15.1. O projeto Eclipse

Por ocasião da mudança de implementação para o JPA, criamos um novo projeto no Eclipse para não sobrecarregar o projeto existente. De fato, o novo projeto utiliza bibliotecas de persistência que podem entrar em conflito com as do Hibernate:

  • em [1]: a pasta [<exemples>/toplink/direct/personnes-entites] contém o projeto Eclipse. Importe-o.
  • em [2]: o projeto [toplink-personnes-entites] importado. Ele é idêntico (foi obtido por cópia) ao projeto [hibernate-personne-entites], com exceção de dois detalhes:
    • o arquivo [META-INF/persistence.xml] [3] agora configura uma camada JPA / Toplink
    • a biblioteca [jpa-hibernate] foi substituída pelas bibliotecas [jpa-toplink], [4] e [5] (conforme parágrafo 1.5).
  • em [6]: a pasta [conf] contém uma versão do arquivo [persistence.xml] para cada SGBD.
  • em [7]: a pasta [ddl], que conterá os scripts SQL para geração do esquema do banco de dados.

Sabemos que a camada JPA é configurada pelo arquivo [META-INF/persistence.xml]. Este, por sua vez, configura agora uma implementação JPA / Toplink. Seu conteúdo para uma camada JPA interligada com o SGBD e o 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">
        <!--  provedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classes persistentes -->
        <class>entites.Personne</class>
        <!-- propriedades da unidade de persistência -->
        <properties>
            <!-- conexão 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" />
            <!--  servidor de aplicativos -->
            <property name="toplink.target-server" value="None" />
            <!--  geração de esquema -->
            <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: não sofreu alterações
  • linha 5: o provedor agora é Toplink. A classe mencionada aqui será encontrada na biblioteca [jpa-toplink] ([1] abaixo):
  • linha 7: a tag <class> serve para nomear todas as classes @Entity do projeto; aqui, apenas a classe Personne. O Hibernate possuía uma opção de configuração que nos poupava de nomear essas classes. Ele explorava o arquivo classpath do projeto para localizar as classes @Entity.
  • linha 9: a tag <properties> que define propriedades específicas da implementação JPA utilizada, neste caso, o Toplink.
  • linhas 11-14: configuração da conexão JDBC com o SGBD MySQL5
  • linhas 15-18: configuração do pool de conexões JDBC gerenciado nativamente pelo Toplink:
  • linhas 15, 16: número máximo e mínimo de conexões no pool de conexões de leitura. Padrão (2,2)
  • linhas 17 e 18: número máximo e mínimo de conexões no pool de conexões de gravação. Padrão (10,2)
  • linha 20: o SGBD de destino. A lista de SGBD utilizáveis está disponível no pacote [oracle.toplink.essentials.platform.database] (ver [2] acima). O SGBD MySQL5 não está presente na lista [2]; por isso, optou-se pelo MySQL4. O Toplink oferece suporte a um número ligeiramente menor de SGBD do que o Hibernate. Assim, dos sete SGBD utilizados em nossos exemplos, o Firebird não é suportado. Também não encontramos o Oracle na lista. Na verdade, ele está em outro pacote ([3], mencionado acima). Se, nesses dois pacotes, o SGBD de destino for designado pela classe <Sgbd>Platform.class, a tag será escrita da seguinte forma:

            <property name="toplink.target-database" value="<Sgbd>" />
  • linha 22: define o servidor de aplicativos caso o aplicativo seja executado nesse tipo de servidor. Valores possíveis atuais (None, OC4J_10_1_3, SunAS9). Padrão (None).
  • linhas 24-28: quando a camada JPA for inicializada, ela será solicitada a limpar o banco de dados definido pela conexão JDBC das linhas 11-14. Assim, partiremos de um banco vazio.
    • linha 24: solicita-se ao Toplink que execute um drop seguido de um create nas tabelas do esquema do banco de dados
    • linha 25: solicitaremos ao Toplink que gere os scripts SQL das operações drop e create. O application-location define a pasta na qual esses scripts serão gerados. Padrão: (pasta atual).
    • linha 26: nome do script SQL das operações create. Padrão: createDDL.jdbc.
    • linha 27: nome do script SQL das operações drop.. Padrão: dropDDL.jdbc.
    • linha 28: modo de geração do esquema (Padrão: both):
      • both: scripts e banco de dados
      • database: somente banco de dados
      • sql-script: apenas scripts
  • linha 30: desativam-se (OFF) os registros do Toplink. Os diferentes níveis de login disponíveis são os seguintes: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. Padrão: INFO.

Consulte a URL [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html] para obter uma definição completa 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]:

  • executar o SGBD, aqui MySQL5
  • executar o [InitDB]
  • no [1]: a exibição do console. Encontramos os resultados já obtidos com o JPA / Hibernate.
  • em [3]: abrimos a perspectiva [SQL Explorer] e, em seguida, abrimos a conexão [mysql5-jpa]
  • em [4]: a árvore da base de dados jpa. Percebe-se que a execução de [InitDB] criou duas tabelas: [jpa01_personne], que era esperada, e a tabela [sequence], que não era esperada.
  • em [5]: a estrutura da tabela [jpa01_personne] e, em [6], seu conteúdo
  • em [7]: a estrutura da tabela [sequence] e, em [8], seu conteúdo.

O arquivo de configuração [persistence.xml] solicitava a geração dos scripts da DDL:


            <!--  geração de esquema -->
            <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 verificar 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 da tabela [jpa01_personne]. Observa-se que o Toplink não utilizou o atributo autoincrement para a chave primária ID. Isso faz com que não haja um incremento automático dessa chave durante as inserções de linhas.
  • linha 2: a DDL da tabela [sequence]. Seu nome parece indicar que o Toplink utiliza essa tabela para gerar os valores da 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: exclusão da tabela [jpa01_personne]
  • linha 2: exclusão de uma linha específica da tabela [SEQUENCE]. A tabela em si não é excluída, nem as demais linhas que ela possa conter.

Para saber mais sobre a função da tabela [SEQUENCE], ativa-se, na tabela [persistence.xml], os logs do Toplink no nível FINE, um nível que registra os comandos SQL emitidos pelo Toplink:


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

Reexecuta-se o InitDB. Abaixo, foi mantida apenas uma visão parcial da exibição do console:


...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Conexão(30708295)--Thread(Thread[main,5,main])--Conectado: 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)--Conexão(19255406)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Conexão(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) padrão 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)--Conexão(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)--Tópico(Thread[main,5,main])--Exceção [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/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)--Conexão(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)--Conexão(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)--Conexão(30708295)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) valores ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Conexão(14069849)--Thread(Thread[main,5,main])--excluir de jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Conexão(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)--Conexão(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)--Conexão(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)--Conexão(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)--Conexão(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)--Conexão(30708295)--Thread(Thread[main,5,main])--desconexão
[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 bem-sucedido
...
terminé ...
  • linhas 2-5: uma conexão com o SGBD e seus parâmetros. Na verdade, os logs mostram que, na realidade, o Toplink cria três conexões com o SGBD. Seria necessário verificar se esse número está relacionado a algum dos valores de configuração utilizados para o pool de conexõ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: exclusão da tabela [jpa01_personne]. Isso é normal, já que o arquivo [persistence.xml] solicita a limpeza do banco de dados JPA.
  • linha 8: criação da tabela [jpa01_personne]. Observa-se que a chave primária ID não possui o atributo autoincrement.
  • linha 9: criação da tabela [SEQUENCE], que já existe, pois foi criada na execução anterior.
  • linhas 10-13: o Toplink sinaliza o erro na criação da tabela [SEQUENCE].
  • linhas 15-18: o Toplink limpa a tabela [SEQUENCE]. Após essa limpeza, a tabela [SEQUENCE] possui uma linha (SEQ_NAME, SEQ_COUNT) com os valores ('SEQ_GEN', 1).
  • linha 18: a tabela [jpa01_personne] é esvaziada.
  • linhas 19-20: o Toplink transfere a única linha em que SEQ_NAME = 'SEQ_GEN' da tabela [SEQUENCE], do valor ('SEQ_GEN', 1) para o valor ('SEQ_GEN', 51)
  • linha 21: o Toplink recupera o valor 51 da linha ('SEQ_GEN', 51) da tabela [SEQUENCE].
  • linhas 24-27: o Toplink insere na tabela [jpa01_personne] as duas pessoas 'Martin' e 'Durant'. Há um mistério aqui: as chaves primárias dessas duas linhas recebem os valores 2 e 3, sem que se saiba como esses valores foram obtidos. Não se sabe se o valor SEQ_COUNT (51), obtido na linha 21, serviu para alguma coisa. Observe-se que o valor da versão das linhas é 1, enquanto o Hibernate começava em 0.
  • linha 28: o Toplink gera o SELECT para obter todas as linhas da tabela [jpa01_personne]
  • linhas 29-30: linhas exibidas pelo cliente Java
  • linhas 31-32: o Toplink encerra uma conexão. Ele repetirá a operação para cada uma das conexões abertas inicialmente.

No final das contas, não se sabe exatamente qual é a função da tabela [SEQUENCE], mas parece que ela desempenha algum papel na geração dos valores da chave primária ID. Ao selecionar o nível de log mais detalhado, FINEST, ficamos sabendo um pouco mais sobre a função da tabela [SEQUENCE].


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

Abaixo, mantivemos apenas os logs 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])--Executar consulta ValueReadQuery()
[TopLink Fine]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Conexão(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)--Conexão(13301441)--Thread(Thread[main,5,main])--pré-alocação de sequenciamento local para SEQ_GEN: objetos: 50, primeiro: 2, último: 51
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--atribuir sequência ao objeto (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])--Executar consulta DoesExistQuery()
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--PERSIST operação chamada em: [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])--atribuir sequência ao objeto (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])--Executar consulta 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])--Atribuir linha de retorno DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Conexão(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)--Tópico(Thread[main,5,main])--Executar consulta 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])--Atribuir linha de retorno DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Conexão(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 serve para delimitar um intervalo de valores para a chave primária: [2,51]
  • linha 5: a primeira pessoa recebe o valor 2 como chave primária
  • linha 8: a segunda pessoa recebe o valor 3 como chave primária
  • linha 12: mostra o gerenciamento de versão da primeira pessoa
  • linha 17: o mesmo para a segunda pessoa

O nível de logs [FINEST] também mostra os limites das transações emitidas pelo Toplink. A análise desses logs revela o que o Toplink faz e é uma excelente maneira de compreender a ponte entre objetos e relações.

Concluímos do exposto acima que:

  • que diferentes implementações do JPA gerarão esquemas de banco de dados diferentes. Neste exemplo, o Hibernate e o Toplink não geraram os mesmos esquemas.
  • que os níveis de log FINE, FINER e FINEST do Toplink devem ser utilizados sempre que for necessário esclarecer exatamente o que o Toplink está fazendo.

2.1.15.4. Teste [Main]

Agora executamos o teste [Main]:

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

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


} catch (RuntimeException e1) {
            // tivemos um problema
            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 um NullPointerException, o que sugere que um dos métodos getCause das linhas 4 e 5 retornou um ponteiro null. Uma expressão como [e1.getCause().getCause()] pressupõe que a cadeia de exceções tenha três elementos [e1.getCause().getCause(), e1.getCause(), e1]. Se ela tiver apenas dois, a primeira expressão causará uma exceção.

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


        } catch (RuntimeException e1) {
            // tivemos um problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage());
            try {
...

Ao executar, obtemos então o seguinte resultado:


...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- teste11
[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. As exibições sobre a exceção (linhas 6-10) foram solicitadas pelo código Java (linha 3 do código acima). Vale lembrar que o teste 11 encadeava, em uma mesma transação, várias operações SQL, das quais uma falhava e deveria provocar um rollback da transação. Os estados da tabela [jpa01_personne] antes (linha 3) e depois do teste (linha 12) são idênticos, demonstrando que o rollback ocorreu.

Vale ressaltar aqui um ponto importante: as implementações JPA / Hibernate e JPA / Toplink não são 100% intercambiáveis. Neste exemplo, precisamos alterar o código do cliente JPA para evitar um NullPointerException. Encontraremos esse problema posteriormente e novamente no contexto de uma exceção.

Voltemos à arquitetura de teste do nosso projeto atual:

Anteriormente, o SGBD utilizado no [7] era o MySQL5. Mostramos, com o Oracle, como mudar para o SGBD. De qualquer forma, a alteração a ser feita no projeto do Eclipse é simples (veja abaixo): substituir o arquivo de configuração da camada JPA, persistence.xml [1], por um dos arquivos da pasta conf ([2] e [3]) do projeto.

2.1.16.1. Oracle 10g Express

O Oracle 10g Express é apresentado nos Anexos, no parágrafo 5.7. O arquivo 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">
        <!--  provedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classes persistentes -->
        <class>entites.Personne</class>
        <!-- propriedades da unidade de persistência -->
        <properties>
            <!-- conexão 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" />
            <!--  servidor de aplicativos -->
            <property name="toplink.target-server" value="None" />
            <!--  geração de esquema -->
            <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>

Essa configuração é idêntica à realizada para o SGBD e o MySQL5, com as seguintes diferenças:

  • linhas 11-14, que configuram a conexão JDBC com o banco de dados
  • linha 20: que define o SGBD como destino
  • linha 25: que define a pasta de geração dos scripts SQL do DDL

Para executar o teste [InitDB]:

  • execute o SGBD Oracle
  • colocar conf/oracle/persistence.xml em META-INF/persistence.xml
  • execute o aplicativo [InitDB]

Obtêm-se os seguintes resultados no console e na perspectiva [SQL Explorer]:

  • [1]: exibição no console
  • [2]: a conexão [oracle-jpa] no SQL Explorer
  • [3]: o banco de dados jpa
  • [4]: InitDB criou duas tabelas: JPA01_PERSONNE e SEQUENCE, assim como no caso de MySQL5. Às vezes, no [4], aparecem tabelas [BIN*]. Elas correspondem a tabelas excluídas. Para observar esse fenômeno, basta reexecutar o [InitDB]. A fase de inicialização da camada JPA inclui uma limpeza do banco de dados jpa, durante a qual a tabela [JPA01_PERSONNE] é excluída:

Em [A], surge uma tabela [BIN]. O Oracle não exclui definitivamente uma tabela que tenha passado por um drop, mas a coloca em uma lixeira [Recycle Bin]. Essa lixeira é visível como [B] com a ferramenta SQL Developer descrita no parágrafo 5.7.4. No [B], é possível esvaziar a tabela [JPA01_PERSONNE] que está na lixeira. Isso esvazia a lixeira [C]. Se, no SQL Explorer, atualizarmos (clique com o botão direito / Refresh) as tabelas, veremos que a tabela BIN não está mais presente ([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! Agora, o leitor está convidado a executar o aplicativo [Main] no Oracle.

2.1.16.2. Os demais SGBD

Não abordaremos muito os demais SGBD. Basta repetir o procedimento seguido para o Oracle. Observe os seguintes pontos:

  • independentemente do SGBD, o Toplink sempre utiliza a mesma técnica para gerar os valores da chave primária ID da tabela [JPA01_PERSONNE]: ele utiliza a tabela [SEQUENCE] detalhada acima.
  • O Toplink não reconhece o SGBD do Firebird. Existe um banco de dados genérico para esses casos:
                <property name="toplink.target-database" value="Auto" />

Com essa base genérica chamada [Auto], os testes com o Firebird falham devido a erros de sintaxe SQL. O Toplink utiliza, para a chave primária ID, um tipo SQL Number(10) que o Firebird não reconhece. É necessário, então, escolher um SGBD com os mesmos tipos SQL que o Firebird (para este exemplo). Esse é o caso do Apache Derby:


            <!-- conexão 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 original do banco de dados para o SGBD HSQLDB. Ou seja, a diretiva:

            <!--  geração de esquema -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />

falha para o HSQLDB. A causa é um erro de sintaxe na criação da tabela [jpa01_personne]:


[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Conexão(29775659)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Conexão(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)--Tópico(Thread[main,5,main])--Exceção [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/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 NOM VARCHAR(30) UNIQUE NOT NULL não é aceita pelo HSQL. O Hibernate havia utilizado a sintaxe: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).

De modo geral, o Hibernate se mostrou mais eficiente que o Toplink no reconhecimento dos SGBD utilizados nos testes deste documento.

2.1.17. Conclusão

O estudo da @Entity [Personne] termina aqui. Do ponto de vista conceitual, pouco foi feito: estudamos a ponte objeto/relacional no caso mais simples: um objeto @Entity <--> uma tabela. No entanto, esse estudo nos permitiu apresentar as ferramentas que utilizaremos ao longo de todo o documento. Isso nos permitirá avançar um pouco mais rapidamente daqui em diante no estudo dos outros casos da ponte objeto/relacional que iremos abordar:

  • ao @Entity [Personne] anterior, vamos adicionar um campo adresse modelado por uma classe [Adresse]. No que diz respeito ao banco de dados, veremos duas implementações possíveis. Os objetos [Personne] e [Adresse] dão origem a
  • uma única tabela [personne], que inclui o endereço
  • duas tabelas [personne] e [adresse] ligadas por uma relação de chave estrangeira do tipo um-para-um.
  • um exemplo de relação um-para-muitos, em que uma tabela [article] está ligada a uma tabela [categorie] por meio de uma chave estrangeira
  • um exemplo de relação muitos-para-muitos, em que duas tabelas, [personne] e [activite], estão ligadas por uma tabela de junção [personne_activite].

2.2. Exemplo 2: relação um-para-um por meio de uma inclusão

2.2.1. O esquema do banco de dados

 
1
2

    drop table if exists jpa02_personne;

    create table jpa02_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;

  • em [1]: o banco de dados (plug-in Azurri Clay)
  • em [2]: a tabela DDL gerada pelo Hibernate para MySQL5

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

2.2.2. Os objetos @Entity que representam o banco de dados

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


package entites;

...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {

    // campos
    @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;

    // construtores
    public Adresse() {

    }

    public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
    }

    // getters e 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 [Adresse] não se destina a gerar uma tabela; portanto, não possui a anotação @Entity. A anotação @Embeddable indica que a classe se destina a ser incorporada a um objeto @Entity e, portanto, à tabela associada a ele. É por isso que, no esquema do banco de dados, a classe [Adresse] não aparece como uma tabela separada, mas como parte da tabela associada à @Entity [Personne].

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


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;

    // construtores
    public Personne() {
    }
...
}
  • a modificação ocorre nas linhas 33-34. O objeto [Personne] agora possui um campo adresse do tipo Adresse. Isso se aplica ao POJO. A anotação @Embedded destina-se à ponte objeto/relacional. Ela indica que o campo [Adresse adresse] deverá ser encapsulado na mesma tabela que o objeto [Personne].

2.2.3. O ambiente de testes

Realizaremos testes muito semelhantes aos estudados anteriormente. Eles serão realizados no seguinte contexto:

A implementação utilizada é JPA / Hibernate [6]. O projeto Eclipse dos testes é o seguinte:

O projeto Eclipse [1] difere do anterior apenas em seus códigos Java [2]. O ambiente (bibliotecas – persistence.xml – SGBD – pastas de configuração, DDL – script Ant) é o mesmo já analisado anteriormente, especialmente no parágrafo 2.1.5. Esse será sempre o caso para os próximos projetos Hibernate e, salvo exceções, não voltaremos a abordar esse ambiente. Notadamente, os arquivos persistence.xml que configuram a camada JPA/Hibernate para diferentes SGBD são aqueles já analisados e que se encontram na pasta <conf>.

Caso tenha alguma dúvida sobre os procedimentos a serem seguidos, o leitor é convidado a consultar os procedimentos seguidos no estudo anterior.

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

2.2.4. Geração do arquivo DDL do banco de dados

Seguindo as instruções do parágrafo 2.1.7, o DDL obtido para o SGBD MySQL5 é 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 deveria ser incluído na tabela associada à @Entity Personne (linhas 11-17).

2.2.5. InitDB

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


package tests;
...

public class InitDB {

    // constantes
    private final static String TABLE_NAME = "jpa02_hb_personne";

    public static void main(String[] args) throws ParseException {

        // Contexto de persistência
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // recupera-se um EntityManager a partir do EntityManagerFactory anterior
        em = emf.createEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitação
        Query sql1;
        // excluir os elementos da tabela PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
        sql1.executeUpdate();
        // criação de pessoas
        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);
        // criação de endereços
        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");
        // associações pessoa <--> endereço
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // persistência de pessoas
        em.persist(p1);
        em.persist(p2);
        // exibição de pessoas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fim da transação
        tx.commit();
        // fim de EntityManager
        em.close();
        // fim de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}

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

  • [1]: a exibição no console
  • [2]: a tabela [jpa02_hb_personne] na perspectiva SQL Explorer
  • [3] e [4]: sua estrutura e conteúdo.

2.2.6. Página inicial

A classe [Main] é a seguinte:


package tests;

...
import entites.Adresse;
import entites.Personne;

@SuppressWarnings( { "unused", "unchecked" })
public class Main {

    // constantes
    private final static String TABLE_NAME = "jpa02_hb_personne";

    // Contexto de persistência
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = null;

    // objetos compartilhados
    private static Personne p1, p2, newp1;

    private static Adresse a1, a2, a3, a4, newa1, newa4;

    public static void main(String[] args) throws Exception {
        // recupera-se um EntityManager a partir do EntityManagerFactory
        em = emf.createEntityManager();

        // limpeza da base
        log("clean");clean();

        // dump da tabela
        dumpPersonne();

        // teste1
        log("test1"); test1();

        // teste2
        log("test2"); test2();

        // teste3
        log("test3"); test3();

        // teste4
        log("test4"); test4();

        // teste5
        log("test5");test5();

        // fim do contexto de persistência
        if (em != null && em.isOpen())
            em.close();

        // fechamento de EntityManagerFactory
        emf.close();
    }

    // recuperar o EntityManager atual
    private static EntityManager getEntityManager() {
...
    }

    // obter um novo EntityManager
    private static EntityManager getNewEntityManager() {
...
    }

    // exibir o conteúdo da tabela Pessoa
    private static void dumpPersonne() {
...
    }

    // zerar BD
    private static void clean() {
    ...
    }

    // registros
    private static void log(String message) {
...
    }

    // criação de objetos
    public static void test1() throws ParseException {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // criação de pessoas
        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);
        // criação de endereços
        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");
        // associações pessoa <--> endereço
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistência de pessoas
        em.persist(p1);
        em.persist(p2);
        // fim da transação
        tx.commit();
        // dump
        dumpPersonne();
    }

    // modificar um objeto do contexto
    public static void test2() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementa-se o número de filhos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // altera-se seu estado civil
        p1.setMarie(false);
        // o objeto p1 é salvo automaticamente (verificação de alterações)
        // na próxima sincronização (commit ou select)
        // fim da transação
        tx.commit();
        // exibe-se a nova tabela
        dumpPersonne();
    }

    // excluir um objeto pertencente ao contexto de persistência
    public static void test4() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // o objeto anexado p2 é excluído
        em.remove(p2);
        // fim da transação
        tx.commit();
        // exibindo a nova tabela
        dumpPersonne();
    }

    // desvincular, vincular novamente e modificar
    public static void test5() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // reanexando p1 ao novo contexto
        p1 = em.find(Personne.class, p1.getId());
        // fim da transação
        tx.commit();
        // alteração do endereço de p1
        p1.getAdresse().setVille("Paris");
        // exibe-se a nova tabela
        dumpPersonne();
    }

}

Mais uma vez, nada que já não tenha sido visto. A exibição no console é 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]]

Sugere-se ao leitor que estabeleça a relação entre os resultados e o código.

Agora estamos utilizando uma implementação JPA / Toplink:

O novo projeto Eclipse dos testes é o seguinte:

Os códigos Java são idênticos aos do projeto Hibernate anterior. O ambiente (bibliotecas – persistence.xml – SGBD – pastas de configuração, DDL – script Ant) é o mesmo já abordado no parágrafo 2.1.15.2. Esse será sempre o caso para os futuros projetos Toplink e, salvo exceções, não voltaremos a abordar esse ambiente. Em particular, os arquivos persistence.xml que configuram a camada JPA/Toplink para diferentes SGBD são aqueles já analisados e que se encontram na pasta <conf>.

Caso tenha alguma dúvida sobre os procedimentos a serem seguidos, o leitor é convidado a consultar os procedimentos seguidos no estudo anterior.

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

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

  • [1]: a exibição no console
  • [2]: as tabelas [jpa02_tl_personne] e [SEQENCE] na perspectiva 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 por meio de uma chave estrangeira

2.3.1. O esquema do o do banco de dados

1
2

    alter table jpa03_hb_personne 
        drop 
        foreign key FKFBBBFDD05FE379D0;

    drop table if exists jpa03_hb_adresse;

    drop table if exists jpa03_hb_personne;

    create table jpa03_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 jpa03_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;

    alter table jpa03_hb_personne 
        add index FKFBBBFDD05FE379D0 (adresse_id), 
        add constraint FKFBBBFDD05FE379D0 
        foreign key (adresse_id) 
references jpa03_hb_adresse (id);
  • em [1]: o banco de dados. Desta vez, o endereço da pessoa é armazenado em uma tabela própria, [adresse]. A tabela [personne] está vinculada a essa tabela por meio de uma chave estrangeira.
  • em [2]: a tabela DDL gerada pelo Hibernate para a tabela MySQL5:
    • linhas 9-20: a tabela [adresse], que será vinculada à classe [Adresse], que se tornou um objeto @Entity.
    • linha 10: a chave primária da tabela [adresse]
    • linha 30: em vez de um endereço completo, agora encontramos na tabela [personne] o identificador [adresse_id] desse endereço.
    • linhas 34-38: pessoa (adresse_id) é uma chave estrangeira em endereço (id).

2.3.2. Os objetos @Entity que representam o banco de dados

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


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 indica uma relação um-para-um: uma pessoa tem no mínimo e no máximo um endereço. O atributo cascade = CascadeType.ALL significa que qualquer operação (persist, merge, remove) na @Entity [Personne] deve ser propagada para a @Entity [Adresse]. Do ponto de vista do contexto de persistência, isso significa o seguinte. Se p é uma pessoa e a é seu endereço:
      • uma operação explícita em.persist(p) resultará em uma operação implícita em.persist(a)
      • uma operação explícita em.merge(p) resultará em uma operação implícita em.merge(a)
      • uma operação em.remove(p) explícita resultará em uma operação em.remove(a) implícita

A experiência mostra que essas cascatas implícitas não são a solução milagrosa. O desenvolvedor acaba esquecendo o que elas fazem. Pode-se preferir operações explícitas no código. Existem diferentes tipos de cascata. 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, neste caso, como valor uma matriz de constantes que especifica os tipos de cascatas desejadas.

O atributo fetch=FetchType.LAZY solicita ao Hibernate que carregue a dependência no último momento. Ao inserir uma lista de pessoas no contexto de persistência, nem sempre se deseja incluir seus endereços. Por exemplo, talvez se queira esse endereço apenas para uma pessoa específica escolhida por um usuário por meio de uma interface web. Já o atributo fetch=FetchType.EAGER solicita o carregamento imediato das dependências.

  • (continuação)
    • linha 33: a anotação @JoinColumn define a chave estrangeira que a tabela da @Entity [Personne] possui na tabela da @Entity [Adresse]. O atributo name define o nome da coluna que serve como chave estrangeira. O atributo unique=true impõe a relação um-para-um: não é possível ter o mesmo valor duas vezes na coluna [adresse_id]. O atributo nullable=false obriga uma pessoa a ter um endereço.

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


package entites;

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

    // campos
    @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;

    // construtores
    public Adresse() {

    }
...
}
  • linha 4: a classe [Adresse] torna-se um objeto @Entity. Portanto, ela será o objeto de uma tabela no banco de dados.
  • linhas 9-12: como qualquer objeto @Entity, [Adresse] possui uma chave primária. Ela foi denominada Id e apresenta as mesmas anotações (padrão) da chave primária Id da @Entity [Personne].
  • linhas 39-40: a relação um-para-um com a @Entity [Personne]. Há várias sutilezas aqui:
    • em primeiro lugar, o campo personne não é obrigatório. Ele nos permite, a partir de um endereço, identificar a única pessoa que possui esse endereço. Se não desejássemos essa facilidade, o campo personne não existiria e tudo funcionaria normalmente.
    • A relação um-para-um que vincula as duas entidades [Personne] e [Adresse] já foi configurada na @Entity [Personne]:

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

Para que as duas configurações um-a-um não entrem em conflito entre si, uma é considerada como principale e a outra como inverse. É a relação denominada principale que é gerenciada pela ponte objeto/relacional. A outra relação, denominada inverse, não é gerenciada diretamente: ela é gerenciada indiretamente pela relação principale. Na @Entity [Adresse]:


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

é o atributo mappedBy que define a relação um-para-um acima, a relação inverse da relação principale, que é um-para-um, definida pelo campo adresse da @Entity [Personne].

2.3.3. O projeto Eclipse / Hibernate 1

A implementação JPA utilizada aqui é a do Hibernate. O projeto Eclipse dos testes é o seguinte:

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

2.3.4. Geração do arquivo DDL do banco de dados

Seguindo as instruções do parágrafo 2.1.7, o arquivo DDL obtido para o SGBD e o MySQL5 é aquele mostrado no início deste parágrafo.

2.3.5. InitDB

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


package tests;
...
import entites.Adresse;
import entites.Personne;

public class InitDB {

    // constantes
    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 {
        // Contexto de persistência
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // recupera-se um EntityManager a partir do EntityManagerFactory anterior
        em = emf.createEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitação
        Query sql1;
        // excluir os elementos da tabela PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // excluir os elementos da tabela ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // criação de pessoas
        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);
        // criação de endereços
        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");
        // associações entre pessoas e endereços
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // persistência de pessoas e, em cadeia, de seus endereços
        em.persist(p1);
        em.persist(p2);
        // e dos endereços a3 e a4 não vinculados a pessoas
        em.persist(a3);
        em.persist(a4);
        // exibição de pessoas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // exibição de endereços
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }

        // fim da transação
        tx.commit();
        // fim EntityManager
        em.close();
        // fim de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}

Comentamos apenas o que apresenta um interesse novo em relação ao que já foi estudado:

  • linhas 31-32: criam-se duas pessoas
  • linhas 34-37: criam-se 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. Se uma pessoa tem necessariamente um endereço, o inverso não é verdadeiro.
  • linhas 44-45: persistimos as pessoas (p1, p2). Como atribuímos um atributo cascade = CascadeType.ALL à relação um-para-um que vincula uma pessoa ao seu endereço, os endereços (a1, a2) dessas duas pessoas também deveriam receber um persist. É isso que queremos verificar. Para os endereços órfãos (a3, a4), somos obrigados a fazer isso 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] junto com MySQL5 produz os seguintes resultados:

  • [1]: exibição no console
  • [2]: as tabelas [jpa03_hb_*] na perspectiva do SQL Explorer
  • [3]: a tabela de pessoas
  • [4]: a tabela de endereços. Estão todos lá, de fato. Observe-se também a relação entre a coluna [adresse_id] em [3] e a coluna [id] em [4] (chave estrangeira).

2.3.6. Página inicial

A classe [Main] encadeia seis testes que vamos analisar.

2.3.6.1. Teste 1

Este teste é o seguinte:


// criação de objetos
    public static void test1() throws ParseException {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // criação de pessoas
        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);
        // criação de endereços
        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");
        // associações pessoa <--> endereço
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistência de pessoas
        em.persist(p1);
        em.persist(p2);
        // e dos endereços a3 e a4 não vinculados a pessoas
        em.persist(a3);
        em.persist(a4);
        // fim da transação
        tx.commit();
        // exibição das tabelas
        dumpPersonne();
        dumpAdresse();
    }

Este código foi extraído de [InitDB]. Seu 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]

As duas tabelas foram preenchidas.

2.3.6.2. Teste 2

Este teste é o seguinte:


    // modificar um objeto do contexto
    public static void test2() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementa o número de filhos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // altera-se seu estado civil
        p1.setMarie(false);
        // o objeto p1 é salvo automaticamente (verificação de alterações)
        // na próxima sincronização (commit ou select)
        // fim da transação
        tx.commit();
        // exibe-se a nova tabela
        dumpPersonne();
}

Seu 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 número de filhos aumentar em 1, e sua versão passar de 0 para 1

2.3.6.3. Teste 4

Este teste é o seguinte:


    // excluir um objeto pertencente ao contexto de persistência
    public static void test4() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // exclui-se o objeto anexado p2
        em.remove(p2);
        // fim da transação
        tx.commit();
        // exibindo as novas tabelas
        dumpPersonne();
        dumpAdresse();
}
  • linha 9: a pessoa p2 é excluída. Ela tem uma relação em cascata com o endereço a2. Portanto, o endereço a2 também deve ser excluído.

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, presente na linha 3 do teste 1, não está mais presente no teste 4
  • o mesmo ocorre com seu endereço a2, que aparecia na linha 7 do teste 1 e está ausente no teste 4.

2.3.6.4. Teste 5

Este teste é o seguinte:


// desanexar, reanexar e modificar
    public static void test5() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // reanexar p1 ao novo contexto
        p1 = em.find(Personne.class, p1.getId());
        // alteração do endereço de p1
        p1.getAdresse().setVille("Paris");
        // fim da transação
        tx.commit();
        // exibindo as novas tabelas
        dumpPersonne();
        dumpAdresse();
    }
  • linha 4: temos um contexto de persistência novo, portanto vazio.
  • linha 9: insere-se a pessoa p1 nesse contexto. p1 é procurado no banco de dados porque não está no contexto. Os elementos dependentes de p1 (seu endereço), por sua vez, não são recuperados do banco de dados porque foi escrito:

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

Esse é o conceito de “lazy loading” ou “carregamento na hora certa”: as dependências de um objeto persistente só são trazidas para a memória quando são necessárias.

  • linha 11: alteramos o campo “cidade” do endereço de p1. Devido ao getAdresse e caso o endereço de p1 ainda não estivesse no contexto de persistência, ele será trazido para lá por meio de uma leitura do banco de dados.
  • linha 13: valida-se a transação, o que levará à sincronização do contexto de persistência com o banco de dados. Este detectará que o endereço da pessoa p1 foi alterado e o salvará.

A execução de 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 cidade da pessoa p1 (linha 3 do teste 4, linha 10 do teste 5) mudou de Angers (linha 5 do teste 4) para Paris (linha 12 do teste 5).

2.3.6.5. Teste 6

Este teste é o seguinte:


// excluir um objeto Endereço
    public static void test6() {
        EntityTransaction tx = null;
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        tx = em.getTransaction();
        tx.begin();
        // reassocia o endereço a3 ao novo contexto
        a3 = em.find(Adresse.class, a3.getId());
        System.out.println(a3);
        // ela é excluída
        em.remove(a3);
        // fim da transação
        tx.commit();
        // dump da tabela Endereço
        dumpAdresse();
    }
  • linha 5: estamos em um contexto de persistência novo, portanto vazio.
  • linha 10: insere-se o endereço a3 no contexto de persistência
  • linha 13: a remoção é realizada. Era um endereço órfão (não vinculado a uma pessoa). A remoçã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:


// revertida
    public static void test7() {
        EntityTransaction tx = null;
        try {
            // novo contexto de persistência
            EntityManager em = getNewEntityManager();
            // início da transação
            tx = em.getTransaction();
            tx.begin();
            // reassocia o endereço a1 ao novo contexto
            newa1 = em.find(Adresse.class, a1.getId());
            // a endereço a4 é reatribuído ao novo contexto
            newa4 = em.find(Adresse.class, a4.getId());
            // tentando excluí-los — deveria lançar uma exceção, pois não é possível excluir um endereço vinculado a uma pessoa, o que é o caso de newa1
            em.remove(newa4);
            em.remove(newa1);
            // fim da transação
            tx.commit();
        } catch (RuntimeException e1) {
            // ocorreu um problema
            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());
            }
            // estamos abandonando o contexto atual
            em.clear();
        }
        // dump — a tabela Endereço não deve ter sido alterada devido ao rollback
        dumpAdresse();
    }
  • test7: estamos testando um rollback de uma transação
    • linha 6: estamos em um contexto de persistência novo, portanto vazio.
    • linha 11: insere-se o endereço a1 no contexto de persistência, sob a referência newa1
    • linha 13: insere-se o endereço a4 no contexto de persistência, sob a referência newa4
    • linhas 15-16: excluem-se os dois endereços newa1 e newa4. newa1 é o endereço da pessoa p1 e, portanto, no banco de dados, p1 faz referência a newa1 por meio de uma chave estrangeira. A exclusão de newa1 irá, portanto, falhar e gerar uma exceção durante a sincronização do contexto de persistência no commit da transação (linha 18). A transação sofrerá um rollback (linha 25) e, portanto, as duas operações da transação serão revertidas. Portanto, deve-se constatar que o endereço newa4, que poderia ter sido legalmente excluído, não foi excluído.

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 do teste 7 (linhas 12-13) é idêntica à do teste 6 (linhas 4-5). O rollback parece ter ocorrido. Dito isso, a mensagem de erro da linha 9 é um enigma e merece ser investigada. Parece que a exceção que ocorreu não é a esperada. É preciso passar os logs do Hibernate no log4j.properties para o modo DEBUG para entender melhor:

# Opção do registrador raiz
log4j.rootLogger=ERROR, stdout

# Opções de registro do Hibernate (INFO exibe apenas mensagens de inicialização)
log4j.logger.org.hibernate=DEBUG

Constata-se, então, que quando o endereço a1 foi inserido no contexto de persistência, o Hibernate também inseriu nele a pessoa p1, provavelmente devido à relação um-para-um da @Entity [Adresse]:


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

Embora tenhamos solicitado o “LazyLoading” aqui, a dependência [Personne] é, no entanto, carregada imediatamente. Isso provavelmente significa que o atributo fetch=FetchType.LAZY não faz sentido neste contexto. Observa-se, em seguida, que, ao confirmar a transação, o Hibernate preparou a exclusão dos endereços a1 e a4, mas também o salvamento da pessoa p1. E é aí que ocorre a exceção: como a pessoa p1 possui uma cascata em seu endereço, o Hibernate tenta persistir também o endereço a1, embora ele tenha acabado de ser destruído. É o Hibernate que lança a exceção, e não o driver JDBC. Daí a mensagem da linha 9 acima. Além disso, pode-se observar que o rollback da linha 25 nunca é executado, pois a transação ficou inativa. O teste da linha 24, portanto, impede o rollback.

Portanto, não atingimos o objetivo desejado: demonstrar um rollback. Na verdade, nenhum comando SQL foi emitido no banco de dados. Vamos destacar alguns pontos:

  • a importância de ativar logs detalhados para compreender o que o ORM faz
  • embora um ORM possa facilitar a vida do desenvolvedor, ele também pode complicá-la ao ocultar comportamentos que o desenvolvedor precisaria conhecer. Neste caso, a forma como as dependências de uma @Entity são carregadas.

2.3.7. Projeto Eclipse / Hibernate 2

Copiamos e colamos o projeto Eclipse / Hibernate para modificar ligeiramente a configuração dos objetos @Entity:

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

Alteramos apenas a @Entity [Adresse] para que ela não tenha mais uma relação inversa um-para-um com a @Entity [Personne]:


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

    // campos
    @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 = "endereço", fetch=FetchType.LAZY)
//     private Pessoa pessoa;

    // construtores
    public Adresse() {

    }
  • linhas 25-26: a relação inversa @OneToOne é removida. É importante entender que uma relação inversa nunca é indispensável. Apenas a relação principal o é. A relação inversa pode ser utilizada por conveniência. Neste caso, ela permitia obter, de forma simples, o proprietário de um endereço. Uma relação inversa sempre pode ser substituída por uma consulta JPQL. É isso que vamos mostrar no exemplo a seguir.

Os programas de teste são reproduzidos exatamente da mesma forma. O que nos interessa é apenas o teste 7, aquele em que vimos a relação inversa um-para-um em ação. Além disso, adicionamos um teste 8 para mostrar como, mesmo sem a relação inversa Endereço -> Pessoa, ainda é possível recuperar a pessoa com determinado endereço.

O teste 7 não sofre alterações. Sua execução agora apresenta os seguintes resultados (logs desativados):


main : ----------- teste6
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 : ----------- teste7
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, temos de fato a exceção esperada: aquela lançada pelo driver JDBC porque tentamos excluir da tabela [adresse] uma linha referenciada por uma chave estrangeira de uma linha da tabela [personne]. A linha [10] deixa clara a causa do erro.
  • O rollback ocorreu corretamente: ao final do teste 7, a tabela [adresse] (linhas 12-13) é a mesma que tínhamos ao final do teste 6 (linhas 4-5).

Qual é a diferença em relação ao teste 7 do projeto Eclipse anterior? Por que temos aqui uma exceção Jdbc que não ocorreu no teste anterior? Porque a @Entity [Adresse] não tem mais uma relação inversa um-para-um com a @Entity [Personne]; ela é gerenciada de forma isolada pelo Hibernate. Quando o endereço newa1 foi inserido no contexto de persistência, o Hibernate não incluiu nesse contexto a pessoa p1, que possui esse endereço. A exclusão dos endereços newa1 e newa4 foi, portanto, realizada sem que a entidade Personne estivesse no contexto.

Agora, como é que, a partir do endereço newa1, poderíamos identificar a pessoa p1 que possui esse endereço? Essa é uma pergunta legítima. O teste 8 a seguir responde a ela:


// relação inversa um-para-um
    // realizada por uma consulta JPQL
    public static void test8() {
        EntityTransaction tx = null;
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        tx = em.getTransaction();
        tx.begin();
        // a endereço a1 é reatribuído ao novo contexto
        newa1 = em.find(Adresse.class, a1.getId());
        // recupera-se o proprietário desse endereço
        Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
                .getSingleResult();
        // exibindo os dados
        System.out.println("adresse=" + newa1);
        System.out.println("personne=" + p1);
        // fim da transação
        tx.commit();
    }
  • linha 6: novo contexto de persistência vazio
  • linhas 8-9: início da transação
  • linha 11: o endereço a1 é inserido no contexto de persistência e referenciado por newa1.
  • linha 13: recupera-se a pessoa p1, cujo endereço é newa1, por meio de uma consulta JPQL. Sabe-se que [Personne] e [Adresse] estão ligados por uma relação de chave estrangeira. Na classe [Personne], é o campo [adresse], que possui a anotação @OneToOne, que concretiza essa relação. A instrução JPQL “select p from Pessoa p join p.adresse a” realiza uma junção entre as tabelas [personne] e [adresse]. O equivalente SQL gerado em um console do Hibernate (ver exemplos do parágrafo 2.1.12) é o seguinte:
SQL #0 tipos: 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

É possível observar claramente a junção das duas tabelas. Cada pessoa está agora vinculada ao seu endereço. Resta especificar que estamos interessados apenas no endereço newa1. A consulta passa a ser “select p from Pessoa p join p.adresse a where a.id=:adresseId”. Observe-se o uso dos aliases p e a. As consultas JPQL utilizam os aliases de forma intensiva. Assim, a expressão “from Pessoa p join p.adresse a” faz com que uma pessoa seja representada pelo alias p e seu endereço (p.adresse) pelo alias a. A operação de restrição “where a.id=:adresseId” restringe as linhas solicitadas apenas às pessoas p que tenham o valor:adresseId como identificador de seu endereço a. :adresseId é chamado de parâmetro, e a ordem JPQL é uma ordem parametrizada. Na execução, esse parâmetro deve receber um valor. Esse é o método

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

que permite atribuir um valor a um parâmetro identificado por seu nome. Observe-se que setParameter retorna um objeto Query, assim como o método createQuery. Assim, é possível encadear chamadas aos métodos [em.createQuery(...).setParameter(...).getSingleResult(...)], já que os métodos [setParameter, getSingleResult] são métodos da interface Query. O método [getSingleResult] é utilizado para consultas Select que retornam apenas um único resultado. É o caso aqui.

  • linhas 16-17: exibe-se 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. Conclui-se deste exemplo que a relação inversa um-para-um da @entity [Adresse] para a @entity [Personne] não era indispensável. A experiência mostrou, neste caso, que sua remoção resultou em um comportamento mais previsível do código. Isso costuma acontecer.

2.3.8. Console do Hibernate

O teste 8 anterior utilizou um comando JPQL para realizar uma junção entre as entidades Personne e Adresse. Embora sejam análogas à linguagem SQL, as linguagens JPQL, JPA ou HQL do Hibernate exigem um processo de aprendizagem, e o console do Hibernate é excelente para isso. Já a utilizamos no parágrafo 2.1.12 para consultar uma única tabela. Vamos repeti-lo aqui para consultar duas tabelas ligadas por uma relação de chave estrangeira.

Vamos criar um console do Hibernate para nosso projeto atual no Eclipse:

  • [1]: mudamos para a perspectiva [Hibernate Console] (Window / Open Perspective / Other)
  • [2]: criamos uma nova configuração
  • usando o botão [4], selecionamos o projeto Java para o qual a configuração do Hibernate será criada. Seu nome é exibido em [3].
  • Em [5], atribuímos o nome desejado a essa configuração. Aqui, utilizamos o nome do projeto Java.
  • No arquivo [6], indicamos que estamos usando uma configuração JPA para que a ferramenta saiba que deve processar o arquivo [META-INF/persistence.xml]
  • no [7]: indicamos nesse arquivo [META-INF/persistence.xml] que é necessário utilizar a unidade de persistência chamada jpa.
  • No arquivo [8], validamos a configuração.

Em seguida, é necessário executar o SGBD. Aqui, trata-se do MySQL5.

  • no [1]: a configuração criada apresenta uma árvore com três ramos
  • em [2]: o ramo [Configuration] lista os objetos que o console utilizou para se configurar: neste caso, as @Entity Personne e Adresse.
  • em [3]: a Session Factory é um conceito do Hibernate semelhante ao EntityManager do JPA. Ela realiza a ponte entre objetos e banco de dados por meio dos objetos do ramo [Configuration]. Em [3] são apresentados os objetos do contexto de persistência, neste caso, novamente os @Entity Personne e Adresse.
  • Em [4]: o banco de dados acessado por meio da configuração encontrada em [persistence.xml]. Nele, encontramos as tabelas [jpa04_hb_*] geradas pelo nosso projeto Eclipse atual.
  • em [1], cria-se um editor HQL
  • no editor HQL,
    • no [2], escolhe-se a configuração do Hibernate a ser usada, caso haja mais de uma (como é o caso aqui)
    • no [3], digita-se o comando JPQL que se deseja executar; neste caso, o comando JPQL do teste 8
    • em [4], executa-se o comando
    • em [5], obtêm-se os resultados da consulta na janela [Hibernate Query Result].
    • em [6], a janela [Hibernate Dynamic SQL preview] permite visualizar a consulta SQL que foi executada.

Outra maneira de obter o mesmo resultado:

  • em [1]: o comando JPQL realiza a junção das entidades Personne e Adresse. [ref1] denomina essa forma de “junção theta”.
  • em [2]: o equivalente SQL
  • em [3]: o resultado

Uma terceira forma aceita apenas pelo Hibernate (HQL):

  • em [1]: o comando HQL. O JPQL não aceita a notação p.adresse.id. Ele aceita apenas um nível de indireção.
  • em [2]: o equivalente SQL. Percebe-se que ele evita a junção entre tabelas.
  • em [3]: o resultado

Aqui estão outros exemplos:

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

Observe-se que apenas a entidade Personne possui uma relação com a entidade Adresse. O inverso não é mais verdadeiro desde que foi suprimida a relação inversa um-para-um denominada personne na entidade Adresse. Se essa relação inversa existisse, poderíamos ter escrito:

  • em [1]: a lista de endereços com seus proprietários, se houver algum, ou nenhum, caso contrário (junção externa à esquerda: a entidade Adresse, que fornecerá as linhas sem relação com Personne, está à esquerda da palavra-chave join).
  • em [2]: o equivalente a SQL.
  • em [3]: o resultado

Recomendamos vivamente que o leitor pratique a linguagem JPQL com o console do Hibernate.

Agora estamos utilizando uma implementação JPA / Toplink:

O novo projeto Eclipse para os testes é o seguinte:

Os códigos Java são idênticos aos do projeto Hibernate anterior. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf, ddl – script ant) é o mesmo abordado no parágrafo 2.1.15.2. O projeto Eclipse [3] está presente na pasta de exemplos [4]. Vamos importá-lo.

O arquivo <persistence.xml> é alterado em um ponto, o das entidades declaradas:


    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classes persistentes -->
        <class>entites.Personne</class>
        <class>entites.Adresse</class>
        <!-- propriedades da unidade de persistência -->
...
  • linhas 5 e 6: as duas entidades gerenciadas

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

No [1], a exibição no console; no [2], as duas tabelas [jpa04_tl] geradas; no [3], os scripts SQL gerados. 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 de o do banco de dados

1
2

    alter table jpa06_article 
        drop 
        foreign key FKFFBDD9D8ECCE8750;

    drop table if exists jpa06_article;

    drop table if exists jpa06_categorie;

    create table jpa06_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa06_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa06_article 
        add index FKFFBDD9D8ECCE8750 (categorie_id), 
        add constraint FKFFBDD9D8ECCE8750 
        foreign key (categorie_id) 
references jpa06_categorie (id);
  • em [1], o banco de dados, e em [2], sua DDL (MySQL5)

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

2.4.2. Os objetos @Entity que representam o banco de dados

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


package entites;

...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relação principal Artigo (muitos) -> Categoria (um)
    // implementada por uma chave estrangeira (categorie_id) na tabela Artigo
    // 1 Artigo tem necessariamente 1 Categoria (nullable=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;

    // construtores
    public Article() {
    }

    // getters e 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: seu número de versão
  • linhas 17-18: nome do artigo
  • linhas 20-25: relação muitos-para-um que vincula a @Entity Article à @Entity Categorie:
    • linha 23: a anotação ManyToOne. O “Many” refere-se à @Entity Article na qual estamos e o “One” à @Entity Categorie (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 se chamará (name) categorie_id e cada linha deverá ter um valor nessa coluna (nullable=false).
    • linha 25: a categoria à qual o artigo pertence. Quando um artigo for inserido no contexto de persistência, solicita-se que sua categoria não seja inserida imediatamente (fetch=FetchType.LAZY, linha 23). Não se sabe se essa solicitação faz sentido. Veremos.

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


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

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relação inversa Categoria (um) -> Artigo (muitos) da relação Artigo (muitos) -> Categoria (um)
    // cascata de inserção de Categoria -> inserção de Artigos
    // cascata de atualização de Categoria -> atualização de Artigos
    // eliminação em cascata de Categoria -> eliminação de Artigos
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();

    // construtores
    public Categorie() {
    }

    // getters e setters
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }

    // associação bidirecional Categoria <--> Artigo
    public void addArticle(Article article) {
        // o artigo é adicionado à coleção de artigos da categoria
        articles.add(article);
        // o artigo muda de categoria
        article.setCategorie(this);
    }
}
  • linhas 8-11: a chave primária da @Entity
  • linhas 12-14: sua versão
  • linhas 16-17: o nome da categoria
  • linhas 19-24: o conjunto (set) de artigos da categoria
    • linha 23: a anotação @OneToMany indica uma relação um-para-muitos. O “One” refere-se à @Entity [Categorie] na qual estamos, e o “Many” ao tipo [Article] da linha 24: uma (One) categoria possui vários (Many) artigos.
    • linha 23: a anotação é o inverso (mappedBy) da anotação ManyToOne aplicada ao campo categorie da @Entity Article: mappedBy=categoria. A relação ManyToOne definida no campo categorie da @Entity Article é a relação principal. Ela é indispensável. Ela representa a relação de chave estrangeira que vincula a @Entity Article à @Entity Categorie. A relação OneToMany definida no campo articles da @Entity Categorie é a relação inversa. Ela não é indispensável. Trata-se de uma facilidade para obter os artigos de uma categoria. Sem essa facilidade, esses artigos seriam obtidos por meio de uma consulta JPQL.
    • linha 23: cascadeType.ALL determina que as operações (persist, merge, remove) realizadas em uma @Entity Categorie sejam propagadas para seus artigos.
    • linha 24: os artigos de uma categoria serão colocados em um objeto do tipo Set<Article>. O tipo Set não aceita duplicatas. Portanto, não é possível inserir duas vezes o mesmo artigo no objeto Set<Article>. O que significa “o mesmo artigo”? Para indicar que o artigo a é o mesmo que o artigo b, o Java utiliza a expressão a.equals(b). Na classe Object, classe-pai de todas as classes, a.equals(b) é verdadeira se a==b, c.a.d. se os objetos a e b tiverem o mesmo endereço de memória. Pode-se querer dizer que os itens a e b são os mesmos se tiverem o mesmo nome. Nesse caso, o desenvolvedor deve redefinir dois métodos na classe [Article]:
      • equals: que deve retornar verdadeiro se os dois itens tiverem o mesmo nome
      • hashCode: deve retornar 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 retornado por hashCode pode ser qualquer número inteiro. Ele é utilizado em diversos contêineres de objetos, notadamente nos dicionários (Hashtable).

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

  • linha 38: o método [addArticle] permite adicionar um artigo a uma categoria. O método se encarrega de atualizar ambas as extremidades da relação OneToMany que liga [Categorie] a [Article].

2.4.3. O projeto Eclipse / Hibernate 1

A implementação JPA utilizada aqui é a do Hibernate. O projeto Eclipse dos testes é o seguinte:

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

2.4.4. Geração do arquivo DDL do banco de dados

Seguindo as instruções do parágrafo 2.1.7, o DDL obtido para o SGBD MySQL5 é aquele mostrado no início deste exemplo, no parágrafo 2.4.1.

2.4.5. InitDB

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


package tests;

...
public class InitDB {

    // constantes
    private final static String TABLE_ARTICLE = "jpa05_hb_article";

    private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";

    public static void main(String[] args) {
        // Contexto de persistência
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // recupera-se um EntityManager a partir do EntityManagerFactory anterior
        em = emf.createEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitação
        Query sql1;
        // excluir os elementos da tabela ARTICLE
        sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
        sql1.executeUpdate();
        // excluir os elementos da tabela CATEGORIE
        sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
        sql1.executeUpdate();
        // criar três categorias
        Categorie categorieA = new Categorie();
        categorieA.setNom("A");
        Categorie categorieB = new Categorie();
        categorieB.setNom("B");
        Categorie categorieC = new Categorie();
        categorieC.setNom("C");
        // criar 3 artigos
        Article articleA1 = new Article();
        articleA1.setNom("A1");
        Article articleA2 = new Article();
        articleA2.setNom("A2");
        Article articleB1 = new Article();
        articleB1.setNom("B1");
        // associá-los à respectiva categoria
        categorieA.addArticle(articleA1);
        categorieA.addArticle(articleA2);
        categorieB.addArticle(articleB1);
        // salvar as categorias e, em seguida (inserção), os artigos
        em.persist(categorieA);
        em.persist(categorieB);
        em.persist(categorieC);
        // exibição das categorias
        System.out.println("[categories]");
        for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
            System.out.println(p);
        }
        // exibição de artigos
        System.out.println("[articles]");
        for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fim da transação
        tx.commit();
        // fim de EntityManager
        em.close();
        // fim de EntityMangerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}
  • linhas 22-27: as tabelas [article] e [categorie] são esvaziadas. Observe-se que é obrigatório começar pela tabela que possui a chave estrangeira. Se começássemos pela tabela [categorie], excluiríamos categorias referenciadas por linhas da tabela [article], e isso seria rejeitado pela tabela SGBD.
  • linhas 29-34: criam-se três categorias A, B, C
  • linhas 36-41: criam-se três itens: A1, A2, B1 (a letra indica a categoria)
  • linhas 43-45: os três artigos são colocados em suas respectivas categorias
  • linhas 47-49: as três categorias são inseridas no contexto de persistência. Devido à cascata Categoria -> Artigo, seus artigos também serão colocados lá. Portanto, todos os objetos criados estão agora no contexto de persistência.
  • linhas 50-59: o contexto de persistência é solicitado para obter a lista de categorias e artigos. Sabe-se que isso provocará uma sincronização do contexto com o banco de dados. É nesse momento que as categorias e os artigos serão gravados em suas respectivas tabelas.

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

  • [1]: exibição no console
  • [2]: as tabelas [jpa05_hb_*] na perspectiva SQL Explorer
  • [3]: a tabela de categorias
  • [4]: a tabela de itens. Observe-se a relação entre [categorie_id] em [4] e [id] em [3] (chave estrangeira).

2.4.6. Página inicial

A classe [Main] encadeia testes que analisamos, exceto os testes 1 e 2, que reutilizam o código de [InitDB] para inicializar o banco de dados.

2.4.6.1. Teste 3

Este teste é o seguinte:


    // procurar um elemento específico
    public static void test3() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // carregamento de categoria
        Categorie categorie = em.find(Categorie.class, categorieA.getId());
        // exibição da categoria e dos itens associados
        System.out.format("Articles de la catégorie %s :%n", categorie);
        for (Article a : categorie.getArticles()) {
            System.out.println(a);
        }
        // fim da transação
        tx.commit();
}
  • linha 4: temos um contexto de persistência novo, portanto vazio
  • linhas 6-7: início da transação
  • linha 9: a categoria A é trazida do banco de dados para o contexto de persistência
  • linha 11: exibimos a categoria A
  • linhas 12-14: exibimos os itens da categoria A. Aqui fica evidente a utilidade da relação inversa OneToMany, que remete aos itens da @Entity Categorie. Sua presença nos poupa de fazer uma consulta JPQL para solicitar os artigos da categoria A. Para obtê-los, utiliza-se o método get do campo articles.

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: a categoria A
  • linhas 21-22: os dois itens da categoria A

2.4.6.2. Teste4

Este teste é o seguinte:


    // excluir um artigo
    @SuppressWarnings("unchecked")
    public static void test4() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // carregamento do item A1
        Article newarticle1 = em.find(Article.class, articleA1.getId());
        // exclusão do artigo A1 (nenhuma categoria está carregada no momento)
        em.remove(newarticle1);
        // toplink: o artigo deve ser removido de sua categoria, caso contrário, o teste6 trava
        // hibernate: isso não é necessário
        newarticle1.getCategorie().getArticles().remove(newarticle1);
        // fim da transação
        tx.commit();
        // dump dos artigos
        dumpArticles();
}
  • o teste 4 exclui o item A1
  • linha 5: parte-se de um contexto novo e vazio
  • linha 10: o artigo A1 é inserido no contexto de persistência. Lá, ele será referenciado por newarticle1.
  • linha 12: ele é removido do contexto
  • linha 15: as categorias A, B e C e os artigos A1, A2 e B1, mesmo que não sejam mais persistentes, ainda estão na memória. Eles estão simplesmente separados do contexto de persistência. O item A1, que faz parte dos itens da categoria A, é removido dela. Isso permitirá, posteriormente, a reatação da categoria A ao contexto de persistência. Se isso não for feito, a categoria A será reataada a um conjunto de itens, dos quais um foi removido. Isso parece não incomodar o Hibernate, mas causa falha no Toplink.
  • 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 artigo A1 realmente desapareceu.

2.4.6.3. Teste 5

Este teste é o seguinte:


// alteração de 1 artigo
    public static void test5() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // modificação de articleA2
        articleA2.setNom(articleA2.getNom() + "-");
        // articleA2 é reposto no contexto de persistência
        em.merge(articleA2);
        // fim da transação
        tx.commit();
        // exportação dos itens
        dumpArticles();
    }
  • o teste 5 altera o nome do artigo A2
  • linha 4: partimos de um contexto novo e vazio
  • linha 9: altera-se o nome do item destacado A2, que passará a ser “A2-”.
  • linha 11: o item destacado A2 é reanexado ao contexto de persistência. Observe-se que A2 continua sendo um objeto destacado. É o objeto em.merge (articleA2) que agora faz parte do contexto de persistência. Esse objeto não foi armazenado aqui em uma variável, como é habitual. Portanto, ele está inacessível.
  • linha 13: sincronização do contexto de persistência com o banco de dados. O artigo A2 será modificado no banco de dados e seu número de versão passará de N para N+1. A versão em memória isolada articleA2 não é mais válida. O mesmo ocorre com o objeto separado que representa a categoria A, pois este contém articleA2 entre seus artigos.
  • linha 15: exibimos todos os itens para verificar a mudança de 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 artigo A2 teve seu nome alterado corretamente.

2.4.6.4. Teste 6

Este teste é o seguinte:


// alteração de 1 categoria e de seus itens
    public static void test6() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // carregamento de categoria
        categorieA = em.find(Categorie.class, categorieA.getId());
        // lista de itens da categoria A
        for (Article a : categorieA.getArticles()) {
            a.setNom(a.getNom() + "-");
        }
        // alteração do nome da categoria
        categorieA.setNom(categorieA.getNom() + "-");
        // fim da transação
        tx.commit();
        // exportação das categorias e dos itens
        dumpCategories();
        dumpArticles();
}
  • o teste 6 altera o nome da categoria A e de todos os seus artigos
  • linha 4: partimos de um contexto novo e vazio
  • linha 9: busca-se a categoria A no banco de dados. Não se executa um merge a partir do objeto destacado categorieA, pois sabe-se que ele possui uma referência ao artigo A2, que se tornou obsoleto. Portanto, recomeça-se do zero.
  • linhas 11-12: alteramos o nome de todos os artigos da categoria A. Mais uma vez, utilizamos a relação inversa OneToMany por meio do método getArticles.
  • linha 15: o nome da categoria também é alterado
  • linha 17: fim da transação. É realizada uma sincronização do contexto com o banco de dados. Todos os objetos do contexto que foram modificados serão atualizados no banco de dados.
  • linhas 21-22: exibem-se os artigos e as categorias 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 mais uma vez, assim como a categoria A.

2.4.6.5. Teste7

Este teste é o seguinte:


// exclusão de uma categoria
    public static void test7() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistência catégorieB e exclusão em cascata (merge) dos itens associados
        Categorie mergedcategorieB = em.merge(categorieB);
        // exclusão da categoria e, em cascata (delete), dos itens associados
        em.remove(mergedcategorieB);
        // fim da transação
        tx.commit();
        // exportação das categorias e dos artigos
        dumpCategories();
        dumpArticles();
    }
  • o teste 7 remove a categoria B e, consequentemente, seus artigos
  • linha 4: partimos de um contexto novo e vazio
  • linha 9: a categoria B existe na memória como um objeto separado do contexto de persistência. Ela é reintegrada (merge) ao contexto de persistência. Em cadeia, seus itens (o item B1) passarão por um merge e, portanto, serão reintegrados ao contexto de persistência.
  • linha 11: agora que a categoria B está no contexto, é possível excluí-la (remove). Por efeito em cascata, seus artigos também passarão por uma operação remove. Isso é possível porque a operação merge da linha 9 os reintegrou ao contexto de persistência.
  • linha 13: fim da transação. O contexto será sincronizado. Os objetos do contexto que passaram por uma operação remove serão excluídos do banco de dados.
  • linhas 15-16: exibimos os artigos 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 foram efetivamente removidos.

2.4.6.6. Teste 8

Este teste é o seguinte:


// solicitações
    @SuppressWarnings("unchecked")
    public static void test8() {
        // novo contexto de persistência
        EntityManager em = getNewEntityManager();
        // transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // lista de itens da categoria 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();
        // visualizações de itens
        System.out.println("Articles de la catégorie A");
        for (Object a : articles) {
            System.out.println(a);
        }
        // fim da transação
        tx.commit();
    }
  • O teste 7 mostra como recuperar os itens de uma categoria sem passar pela relação inversa. Isso demonstra que ela não é, portanto, indispensável.
  • linha 4: partimos de um contexto novo e vazio
  • linha 10: uma consulta JPQL que solicita todos os artigos de uma categoria cujo nome comece com A
  • linhas 15-17: exibição do resultado 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 sobre o conceito de relação principal / relação inversa que criamos em torno da anotação @ManyToOne (principal) da @Entity [Article] e a relação inversa @OneToMany (inversa) da @Entity [Categorie]. Queremos demonstrar que, se essa última relação não for declarada como inversa da outra, o esquema gerado para o banco de dados será totalmente diferente daquele gerado anteriormente.

No [1], o novo projeto do Eclipse. Em [2], os códigos Java; em [3], o script ant que irá gerar o esquema SQL do banco de dados. O projeto está presente como [4] na pasta de exemplos [5]. Vamos importá-lo.

Alteraremos apenas a @Entity [Categorie] para que sua relação @OneToMany com a @Entity [Article] não seja mais declarada como inversa à relação @ManyToOne que a @Entity [Article] mantém com a @Entity [Categorie]:


...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relação OneToMany não inversa (ausência de mappedby) Categoria (um) -> Artigo (muitos)
    // implementada por uma tabela de junção Categorie_Article para que, a partir de uma categoria
    // seja possível acessar os artigos dessa categoria
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

    // fabricantes
...
  • linhas 18-22: ainda queremos manter a possibilidade de encontrar os artigos de uma determinada categoria por meio da relação @OneToMany da linha 21. Mas queremos conhecer a influência do atributo mappedBy, que transforma uma relação no inverso de uma relação principal definida em outro lugar, em outra @Entity. Aqui, o mappedBy foi removido.

Executamos a tarefa ant-DLL (ver parágrafo 2.1.7) com o SGBD e o MySQL5. O esquema obtido é o seguinte:

Observe os seguintes pontos:

  • foi criada uma nova tabela [categorie_article] [1]. Ela não existia anteriormente.
  • trata-se de uma tabela de junção entre as tabelas [categorie] [2] e [article] [3]. Se os objetos Artigo a1 e a2 pertencerem à categoria c1, encontrar-se-ão na tabela de junção as linhas:
[c1,a1]
[c1,a2]

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

  • A tabela de junção [categorie_article] [1] foi criada pelo Hibernate para que, a partir de um objeto Categoria c, seja possível recuperar os objetos Artigo a pertencentes a c. Foi a relação @OneToMany que forçou a criação dessa tabela. Como não a declaramos como inversa da relação principal @ManyToOne da @Entity Article, o Hibernate não sabia que poderia usar essa relação principal para recuperar os artigos de uma categoria c. Por isso, ele encontrou outra maneira de fazer isso.
  • Com esse exemplo, compreende-se melhor os conceitos das relações principale e inverse. Uma (a inversa) utiliza as propriedades da outra (a principal).

O esquema SQL desta base de dados para MySQL5 é 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]. Observe-se que elas 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 não inversa @OneToMany da @Entity Categorie. Os registros dessa tabela são do tipo [c,a], em que c é a chave primária de uma categoria c e a é a chave primária deum artigo a pertencente à categoria c. A chave primária dessa tabela de junção é constituída pelas 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]

Recomenda-se ao leitor que execute os testes [InitDB] e [Main]. Eles apresentam os mesmos resultados de antes. No entanto, o esquema do banco de dados é redundante e o desempenho será prejudicado em relação à versão anterior. Provavelmente, seria necessário aprofundar essa questão das relações inversas/principais para verificar se a nova configuração não gera, além disso, conflitos decorrentes do fato de termos duas relações independentes para representar a mesma coisa: a relação muitos-para-um que a tabela [article] mantém com a tabela [categorie].

Agora utilizamos uma implementação JPA / Toplink:

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

Os códigos Java são idênticos aos do projeto Hibernate — versão 1 — anterior. O ambiente (bibliotecas – persistence.xml – banco de dados – pastas conf, ddl – script ant) é o mesmo analisado no parágrafo 2.1.15.2. O projeto Eclipse está presente como [3] na pasta de exemplos [4]. Vamos importá-lo.

O arquivo <persistence.xml> [2] é modificado em um ponto, o das entidades declaradas:


        ...
        <!-- classes persistentes -->
        <class>entites.Categorie</class>
        <class>entites.Article</class>
...
  • linhas 3 e 4: as duas entidades gerenciadas

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

No [1], a exibição no console; no [2], as duas tabelas [jpa05_tl] geradas; no [3], os scripts SQL gerados. 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 do [Main] ocorre sem erros.

Este projeto Eclipse foi criado a partir do anterior por meio de cópia. Como foi feito com o Hibernate, remove-se o atributo mappedBy da relação @OneToMany da @Entity Categorie.


@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relação OneToMany não inversa (ausência de mappedby) Categoria (one) ->
    // Artigo (muitos)
    // implementada por uma tabela de junção Categorie_Article para que, a partir
    // uma categoria
    // seja possível acessar vários artigos
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

O esquema SQL gerado para 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 concretiza a relação @OneToMany não inversa anterior.

A execução de [InitDB] ocorre sem erros, mas a de [Main] falha no teste 7, com os seguintes logs (FINEST):

main : ----------- test7
[TopLink Finer]: 2007.06.01 01:41:48.734--ServerSession(15290002)--Thread(Thread[main,5,main])--cliente adquirido
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Mesclar clone com referências Categoria[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Tópico(Tópico[main,5,main])--Registrar o objeto existente Article[6,1,B1]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Tópico(Tópico[main,5,main])--Registrar o objeto existente Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--A operação de remoção foi realizada em: Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Tópico(Tópico[main,5,main])--A operação de remoção foi realizada em: Article[6,1,B1]
[TopLink Finer]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--início do commit da unidade de trabalho
[TopLink Finer]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Conexão(6330655)--Thread(Thread[main,5,main])--início da transação
[TopLink Finest]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Executar consulta DeleteObjectQuery(Artigo[6,1,B1])
[TopLink Fine]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Conexão(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])--Pilha de exceções locais: 
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: o merge na categoria B
  • linha 4: o artigo dependente B1 é inserido no contexto
  • linha 5: o mesmo para a própria categoria B
  • linha 6: o remove na categoria B
  • linha 7: o remove no item B1 (por cascata)
  • linha 8: o commit da transação é solicitado pelo código Java
  • linha 9: uma transação é iniciada — portanto, aparentemente, ela ainda não havia começado.
  • linha 10: o item B1 será excluído por uma operação DELETE na tabela [article]. É aí que está o problema. A tabela de junção [categorie_article] possui uma referência à linha B1 da tabela [article]. A exclusão de B1 em [article] violará uma restrição de chave estrangeira.
  • linhas 13 e seguintes: ocorre a exceção

O que concluir?

  • Mais uma vez, temos um problema de portabilidade entre o Hibernate e o Toplink: o Hibernate havia sido bem-sucedido neste teste
  • O Toplink não lida bem com a situação em que, quando duas relações são, na verdade, inversas uma da outra, uma delas não seja declarada como principal e a outra como inversa. Isso pode ser aceito, pois esse caso representa, na verdade, um erro de configuração. Em nosso exemplo, a tabela [article] não tem relação com a tabela de junção [categorie_article]. Parece, então, 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 do banco de dados

  • em [1], o banco de dados MySQL5

Já conhecemos as tabelas [personne], [2] e [adresse], [3]. Elas foram analisadas no parágrafo 2.3.1. Consideramos a versão em que o endereço da pessoa é objeto de uma tabela própria: [adresse] e [3]. Na tabela [personne], a relação que vincula uma pessoa ao seu endereço é representada por uma restrição de chave estrangeira.

Uma pessoa exerce atividades. Essas atividades estão presentes nas tabelas [activite] e [4]. Uma pessoa pode exercer várias atividades e uma atividade pode ser exercida por várias pessoas. Portanto, uma relação muitos-a-muitos vincula as tabelas [personne] e [activite]. Essa relação é representada pela tabela de junção [personne_activite] [5].

2.5.2. Os objetos @Entity que representam o banco de dados

As tabelas anteriores serão representadas pelas seguintes @Entity:

  • a @Entity Personne representará a tabela [personne]
  • o @Entity Adresse representará a tabela [adresse]
  • a @Entity Activite representará a tabela [activite]
  • a @Entity PersonneActivite representará a tabela [personne_activite]

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

  • uma relação um-para-um liga a entidade Personne à entidade Adresse: uma pessoa p possui um endereço a. A entidade Personne, que possui a chave estrangeira, terá a relação principal, enquanto a entidade Adresse terá a relação inversa.
  • Uma relação muitos-a-muitos liga as entidades Personne e Activite: uma pessoa exerce várias atividades e uma atividade é exercida por várias pessoas. Essa relação poderia ser implementada diretamente por meio de uma anotação @ManyToMany em cada uma das duas entidades, sendo uma declarada como inversa da outra. Essa solução será explorada posteriormente. Aqui, implementamos a relação muitos-para-muitos por meio de duas relações um-para-muitos:
    • uma relação um-para-muitos que liga a entidade Personne à entidade PersonneActivite: uma linha (One) da tabela [personne] é referenciada por várias (Many) linhas da tabela [personne_activite]. A tabela [personne_activite], que contém a chave estrangeira, manterá a relação @ManyToOne principal, e a entidade Personne manterá a relação @OneToMany inversa.
    • uma relação um-para-muitos que conecta a entidade Activite à entidade PersonneActivite: uma linha (One) da tabela [activite] é referenciada por várias (Many) linhas da tabela [personne_activite]. A tabela [personne_activite], que contém a chave estrangeira, manterá a relação @ManyToOne principal, e a entidade Activite manterá a relação @OneToMany inversa.

A @Entity Personne é 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;

    // relação principal Pessoa (one) -> Endereço (one)
    // implementada pela chave estrangeira Pessoa (adresse_id) -> Endereço
    // cascata de inserção Pessoa -> inserção Endereço
    // cascata de atualização Pessoa -> atualização Endereço
    // eliminação em cascata de Pessoa -> eliminação de Endereço
    // uma Pessoa deve ter 1 Endereço (nullable=false)
    // 1 Endereço pertence a apenas 1 Pessoa (único=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relação Pessoa (one) -> PersonneActivite (many)
    // reversão da relação existente PersonneActivite (muitos) -> Pessoa (um)
    // eliminação em cascata Pessoa -> eliminação PersonneActivite
    @OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();

    // construtores

Esta @Entity já é conhecida. Comentamos apenas as relações que ela mantém com as outras entidades:

  • linhas 30-39: uma relação um-para-um @OneToOne com a @Entity Adresse, materializada por uma chave estrangeira [adresse_id] (linha 38) que a tabela [personne] terá na tabela [adresse].
  • linhas 41-45: uma relação um-para-muitos @OneToMany com a @Entity PersonneActivite. Uma pessoa (One) é referenciada por várias (Many) linhas da tabela de junção [personne_activite], representada pela @Entity PersonneActivite. Esses objetos PersonneActivite serão colocados em um tipo Set<PersonneActivite>, em que PersonneActivite é um tipo que definiremos em breve.
  • linha 44: a relação um-para-muitos definida aqui é a relação inversa de uma relação principal definida no campo personne da @Entity PersonneActivite (palavra-chave mappedBy). Temos uma cascata Pessoa -> Atividade nas exclusões: a exclusão de uma pessoa p resultará na exclusão dos elementos persistentes do tipo PersonneActivite encontrados no conjunto p.activites.

A @Entity Adresse é a seguinte:


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

    // campos
    @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, inversa da relação @OneToOne, remete para a @Entity Personne (linhas 37-38 de Personne).

A @Entity Activite é a seguinte


@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {

    // campos
    @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;

    // relação Atividade (um) -> PersonneActivite (muitos)
    // reversão da relação existente PersonneActivite (muitos) -> Atividade (um)
    // eliminação em cascata de Atividade -> eliminação de 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 vincula a @Entity Activite à @Entity PersonneActivite: uma atividade (One) é referenciada por várias (Many) linhas da tabela de junção [personne_activite], representada pela @Entity PersonneActivite. Esses objetos PersonneActivite serão colocados em um tipo Set<PersonneActivite>.
  • linha 22: a relação um-para-muitos definida aqui é a relação inversa de uma relação principal definida no campo activite na @Entity PersonneActivite (palavra-chave mappedBy). Temos uma cascata Atividade -> PersonneActivite para as exclusões: a exclusão da tabela [activite] dauma atividade a resultará na exclusão da tabela de junção [personne_activite] dos elementos persistentes do tipo PersonneActivite encontrados no conjunto a.personnes.

A @Entity PersonneActivite é a seguinte:


@Entity
// tabela de junção
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {

    @Embeddable
    public static class Id implements Serializable {
        // componentes da chave composta
        // aponta para uma Pessoa
        @Column(name = "PERSONNE_ID")
        private Long personneId;

        // aponta para uma Atividade
        @Column(name = "ACTIVITE_ID")
        private Long activiteId;

        // construtores
...

        // getters e setters
...
        // toString
        public String toString() {
            return String.format("[%d,%d]", getPersonneId(), getActiviteId());
        }
    }

    // campos da classe Personne_Activite
    // chave composta
    @EmbeddedId
    private Id id = new Id();

    // relação principal PersonneActivite (muitos) -> Pessoa (um)
    // implementada pela chave estrangeira: personneId (PersonneActivite (muitos) -> Pessoa (um)
    // personneId é, ao mesmo tempo, um elemento da chave primária composta
    // JPA não deve gerenciar essa chave estrangeira (insertable = false, updatable = false), pois isso é feito pela própria aplicação em seu construtor
    @ManyToOne
    @JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
    private Personne personne;

    // relação principal PersonneActivite -> Atividade
    // implementada pela chave estrangeira: activiteId (PersonneActivite (muitos) -> Atividade (uma)
    // activiteId é, ao mesmo tempo, um elemento da chave primária composta
    // JPA não deve gerenciar essa chave estrangeira (insertable = false, updatable = false), pois isso é feito pela própria aplicação em seu construtor
    @ManyToOne()
    @JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
    private Activite activite;

    // construtores
    public PersonneActivite() {

    }

    public PersonneActivite(Personne p, Activite a) {
        // as chaves estrangeiras são definidas pelo aplicativo
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // associações bidirecionais
        this.setPersonne(p);
        this.setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
    }

    // getters e setters
...
    // toString
    public String toString() {
        return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
    }
}

Essa classe é mais complexa do que as anteriores.

  • A tabela [personne_activite] contém registros no formato [p,a], em que p é a chave primária de uma pessoa e a é a chave primária de uma atividade. Toda tabela deve ter uma chave primária, e [personne_activite] não é exceção à regra. Até agora, definimos 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 pratica a atividade a1. Não é possível encontrar essa mesma linha uma segunda vez na tabela. Assim, o par (p, a) é um bom candidato a chave primária. Isso é chamado de chave primária composta.
  • linhas 30-31: a chave primária composta. A anotação @EmbeddedId (normalmente seria @Id) é análoga à notação @Embedded aplicada ao campo Adresse de uma pessoa. Nesse último caso, isso significava que o campo Adresse era objeto de uma classe externa, mas deveria ser inserido na mesma tabela que a pessoa. Aqui, o significado é o mesmo; a única diferença é que, para indicar que se trata da chave primária, a notação passa a ser @EmbeddedId.
  • linha 31: um objeto vazio representando a chave primária id é criado assim que o objeto [PersonneActivite] é criado. A classe que representa a chave primária é definida nas linhas 7 a 26 como uma classe pública, estática e interna à classe [PersonneActivite]. O fato de ela ser pública e estática é imposto pelo Hibernate. Se substituirmos “public static” por “private,”, ocorre uma exceção e vemos na mensagem de erro associada que o Hibernate tentou executar a instrução “new PersonneActivite$Id”. Portanto, a classe Id precisa ser tanto estática quanto pública.
  • linha 6: a classe Id da chave primária é declarada como @Embeddable. Lembramos que a chave primária id da linha 31 foi declarada como @EmbeddedId. A classe correspondente deve, portanto, ter a anotação @Embeddable.
  • Dissemos que a chave primária da tabela [personne_activite] era composta pelo 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 (personneId) e na linha 15 (activiteId). As colunas associadas a esses dois campos são denominadas: PERSONNE_ID para a pessoa e ACTIVITE_ID para a atividade.
  • linha 31: a chave primária foi definida com suas duas colunas (PERSONNE_ID, ACTIVITE_ID). Não há outras colunas na tabela [personne_activite]. Resta apenas definir as relações existentes entre a @Entity PersonneActivite que estamos descrevendo atualmente e as outras @Entity do esquema relacional. Essas relações refletem as restrições de chaves estrangeiras que a tabela [personne_activite] possui com as outras tabelas.
  • linhas 33-39: definem a chave estrangeira que a tabela [personne_activite] possui na tabela [personne]
  • linha 37: a relação é do tipo @ManyToOne: uma linha (One) da tabela [personne] é referenciada por várias (Many) linhas da tabela [personne_activite].
  • linha 38: nomeia-se a coluna-chave estrangeira. Utiliza-se o mesmo nome dado ao componente “pessoa” da chave estrangeira (linha 10). Os atributos insertable=false, updatable=false servem para impedir que o Hibernate gerencie a chave estrangeira. Esta é, de fato, o componente de uma chave primária calculada pela aplicação, e o Hibernate não deve intervir.
  • linhas 41-47: definem a chave estrangeira que a tabela [personne_activite] possui na tabela [activite]. As explicações são as mesmas apresentadas anteriormente.
  • linhas 54-63: construtor de um objeto PersonneActivite a partir de uma pessoa p e de uma atividade a. Lembramos que, na construção de um objeto PersonneActivite, a chave primária id da linha 31 apontava para um objeto Id vazio. As linhas 56-57 atribuem um valor a cada um dos campos (personneId, activiteId) do objeto Id. Esses valores são, respectivamente, 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) agora possui, portanto, um valor.
  • linha 59: o campo personne da linha 39 recebe o valor p
  • linha 60: o campo activite da linha 47 recebe o valor a
  • Um objeto [PersonneActivite] foi criado e inicializado. Atualizamos as relações inversas entre as @Entity Personne (linha 61) e Activite (linha 62) e a @Entity PersonneActivite que acaba de ser criada.

Concluímos a descrição das entidades do banco de dados. Estamos diante de uma situação complexa, mas, infelizmente, frequente. Veremos que existe outra configuração possível para a camada JPA que oculta parte dessa complexidade: a tabela de junção torna-se implícita, construída e gerenciada pela camada JPA. Escolhemos aqui a solução mais complexa, mas que permite que o esquema relacional evolua. Assim, ela permite adicionar 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. A camada [ref1] recomenda a solução que estamos analisando. Foi no [ref1] que foram encontradas as informações que permitiram a elaboração desta solução.

2.5.3. O projeto Eclipse / Hibernate

A implementação JPA utilizada aqui é a do Hibernate. O projeto Eclipse dos testes é o seguinte:

 

Image

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

2.5.4. Geração do arquivo DDL do banco de dados

Seguindo as instruções do parágrafo 2.1.7, o DDL obtido 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 [activite]
  • linhas 28-39: a tabela [adresse]
  • linhas 41-51: a tabela [personne]
  • linhas 53-57: a tabela de junção [personne_activite]. Observe-se a chave composta (linha 56)
  • linhas 59-63: a chave estrangeira da tabela [personne] para a tabela [adresse]
  • linhas 65-69: a chave estrangeira da tabela [personne_activite] para a tabela [activite]
  • linhas 71-75: a chave estrangeira da tabela [personne_activite] para a tabela [personne]

2.5.5. InitDB

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


package tests;

...
public class InitDB {

    // constantes
    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 {
        // Contexto de persistência
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // recupera-se um EntityManager a partir do EntityManagerFactory
        // anterior
        em = emf.createEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitação
        Query sql1;
        // excluir os elementos da tabela PERSONNE_ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
        sql1.executeUpdate();
        // excluir os elementos da tabela PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // excluir os elementos da tabela ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
        sql1.executeUpdate();
        // excluir os elementos da tabela ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // criação de atividades
        Activite act1 = new Activite();
        act1.setNom("act1");
        Activite act2 = new Activite();
        act2.setNom("act2");
        Activite act3 = new Activite();
        act3.setNom("act3");
        // persistência de atividades
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // criação de pessoas
        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);
        // criação de endereços
        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");
        // associações entre pessoas e endereços
        p1.setAdresse(adr1);
        adr1.setPersonne(p1);
        p2.setAdresse(adr2);
        adr2.setPersonne(p2);
        p3.setAdresse(adr3);
        adr3.setPersonne(p3);
        // persistência de pessoas e, portanto, dos endereços associados
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // persistência do endereço a4 não vinculado a uma pessoa
        em.persist(adr4);
        // exibição de pessoas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // exibição de endereços
        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);
        }
        // associações pessoa <--> atividade
        PersonneActivite p1act1 = new PersonneActivite(p1, act1);
        PersonneActivite p1act2 = new PersonneActivite(p1, act2);
        PersonneActivite p2act1 = new PersonneActivite(p2, act1);
        PersonneActivite p2act3 = new PersonneActivite(p2, act3);
        // persistência das associações pessoa <--> atividade
        em.persist(p1act1);
        em.persist(p1act2);
        em.persist(p2act1);
        em.persist(p2act3);
        // exibição de pessoas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // exibição de endereços
        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);
        }
        // fim da transação
        tx.commit();
        // fim de EntityManager
        em.close();
        // fim de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}
  • linhas 27-38: as tabelas [personne_activite], [personne], [adresse] e [activite] são esvaziadas. Observe-se que é necessário começar pelas tabelas que possuem chaves estrangeiras.
  • linhas 40-45: criam-se três atividades act1, act2 e act3
  • linhas 47-49: elas são inseridas no contexto de persistência.
  • linhas 51-53: criam-se três pessoas: p1, p2 e p3.
  • linhas 55-58: criam-se quatro endereços, de adr1 a adr4.
  • linhas 60-65: os endereços adri são associados às pessoas pi. Há, em cada caso, duas operações a serem realizadas, pois a relação Pessoa <-> Endereço é bidirecional.
  • linhas 67-69: as pessoas p1 a p3 são inseridas no contexto de persistência. Devido à cascata Pessoa -> Endereço, o mesmo ocorrerá com os endereços de adr1 a adr3.
  • linha 71: o quarto endereço adr4, que não está associado a uma pessoa, é colocado explicitamente no contexto de persistência.
  • linhas 73-85: o contexto de persistência é consultado para obter as listas das entidades do tipo [Personne], [Adresse] e [Activite]. Sabemos que essas consultas provocarão a sincronização do contexto com o banco de dados: as entidades criadas serão inseridas no banco de dados e receberão sua chave primária. É importante compreender isso para o que vem a seguir.
  • linhas 87-90: criam-se 4 associações Pessoa <-> Atividade. Seus nomes indicam qual pessoa está vinculada a qual atividade. Talvez nos lembremos de que a chave primária de uma entidade PersonneActivite é uma chave composta formada pela chave primária de uma pessoa e pela de uma atividade. Portanto, é porque as entidades Personne e Activite obtiveram suas chaves primárias durante uma sincronização anterior que essa operação é possível.
  • linhas 92-95: essas quatro associações são inseridas no contexto de persistência.
  • linhas 87-86: o contexto de persistência é consultado para obter a lista das entidades dos tipos [Personne], [Adresse], [Activite] e [PersonneActivite]. Sabe-se que essas consultas provocarão a sincronização do contexto com o banco de dados: as entidades PersonneActivite criadas serão inseridas no banco de dados.

A execução de [InitDB] junto com MySQL5 resulta na seguinte exibição no console:

[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 observar que, nas linhas 15-16, as pessoas p1 e p2 têm o número de versão igual a 1 e que o mesmo ocorre, nas linhas 24-26, para as três atividades. Vamos tentar entender.

Nas linhas 2 a 4, os números de versão das pessoas estão em 0 e, nas linhas 11 a 13, os números de versão das atividades estão em 0. As exibições anteriores ocorrem antes da criação das relações Pessoa <-> Atividade. Nas linhas 87-90 do código Java, são criadas relações entre as pessoas p1 e p2 e as atividades act1, act2, act3. Elas são criadas por meio do construtor da @Entity PersonneActivite (ver parágrafo 2.5.2). A análise do código desse construtor mostra que, quando uma pessoa p está vinculada a uma atividade a:

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

Assim, quando se escreve new PersonneActivite(p,a), a pessoa p e a atividade a sofrem uma modificação na memória. Quando nas linhas 97-113 de [InitDB], o contexto de persistência é sincronizado com o banco de dados, JPA / o Hibernate detecta que os elementos persistentes p1, p2, act1, act2 e act3 foram alterados. Essas alterações devem ser feitas no banco de dados. Elas estão, na verdade, registradas na tabela de junção [personne_activite], mas o JPA / Hibernate ainda assim incrementa o número de versão de cada um dos elementos persistentes alterados.

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

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

2.5.6. Página inicial

A classe [Main] encadeia testes que analisamos, exceto o teste 1, que utiliza o código de [InitDB] para inicializar o banco de dados.

2.5.6.1. Teste 2

Este teste é o seguinte:


// exclusão Pessoa p1
    public static void test2() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // remoção de dependências em p1: não é necessário para o Hibernate, mas
        // indispensável para o TopLink
        act1.getPersonnes().remove(p1act1);
        act2.getPersonnes().remove(p1act2);
        // remoção da pessoa p1
        em.remove(p1);
        // fim da transação
        tx.commit();
        // exibindo as novas tabelas
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • linha 4: utiliza-se o contexto de persistência de test1, onde a pessoa p1 é um objeto do contexto.
  • linha 13: exclusão da pessoa p1. Devido ao atributo:
    • cascadeType.ALL sobre Adresse, o endereço da pessoa p1 será excluído
    • cascadeType.REMOVE em PersonneActivite, as atividades da pessoa p1 serão excluídas.
  • linhas 10-11: eliminam-se as dependências que as outras entidades têm em relação à pessoa p1, que será excluída na linha 13. As atividades act1 e act2 são realizadas pela pessoa p1. Os vínculos foram criados pelo construtor da entidade PersonneActivite, cujo código é o seguinte:

    public PersonneActivite(Personne p, Activite a) {
        // as chaves estrangeiras são definidas pela aplicação
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // associações bidirecionais
        setPersonne(p);
        setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
}

na linha 9, a atividade a recebe um elemento adicional do tipo PersonneActivite em seu conjunto personnes. Esse elemento é do tipo (p,a) para indicar que a pessoa p exerce a atividade a. Em test1 de [Main], foram criados dois links: (p1,act1) e (p1,act2). As linhas 10 e 11 de test2 removem essas dependências. É importante observar que o Hibernate funciona sem a remoção dessas dependências na entidade p1, mas o Toplink não.

  • linhas 17-20: exibimos todas as tabelas

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, presente em test1 (linha 3), não está mais presente ao final de test2 (linhas 22-23)
  • o endereço adr1 da pessoa p1, presente em test1 (linha 11) deixa de existir após a execução de test2 (linhas 29-31)
  • as atividades (p1,act1) (linha 16) e (p1,act2) (linha 18) da pessoa p1, presentes em test1, não estão mais presentes após a execução de test2 (linhas 33-34)

2.5.6.2. Teste 3

Este teste é o seguinte:


// exclusão da atividade act1
    public static void test3() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // remoção de dependências na act1: não é necessário para o Hibernate, mas
        // indispensável para o TopLink
        p2.getActivites().remove(p2act1);
        // remoção da atividade act1
        em.remove(act1);
        // fim da transação
        tx.commit();
        // exibição das novas tabelas
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • linha 4: utiliza-se o contexto de persistência de test2
  • linha 12: exclusão da atividade act1. Devido ao atributo:
    • cascadeType.REMOVE em PersonneActivite, as linhas (p, act1) da tabela [personne_activite] serão excluídas.
  • linha 10: antes de retirar act1 do contexto de persistência, são removidas as dependências que outras entidades possam ter em relação a esse objeto persistente. Após a exclusão da pessoa p1 no teste anterior, apenas a pessoa p2 exerce a atividade act1.
  • linhas 13-16: exibem-se todas as tabelas

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]
  • em test2, a atividade act1 existe (linha 6). No teste3, ela não existe mais (linhas 21-22)
  • em test2, o link (p2,act1)) existe (linha 14). Em test3, ele não existe mais (linha 28)

2.5.6.3. Test4

Este teste é o seguinte:


// recuperação das atividades de uma pessoa
    public static void test4() {
        // contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // recuperação da pessoa p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
        // analisam-se suas atividades
        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);
        }
        // passa-se pela relação inversa de p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
        // analisando suas atividades
        for (PersonneActivite pa : p2.getActivites()) {
            System.out.println(pa.getActivite().getNom());
        }
        // fim da transação
        tx.commit();
    }
  • o teste 4 exibe as atividades da pessoa p2.
  • linha 4: parte-se de um contexto novo e vazio
  • linhas 12-14: exibimos os nomes das atividades realizadas pela pessoa p2 por meio de uma consulta JPQL.
    • É realizada uma junção entre Activite (a) e PersonneActivite (pa) (join a.personnes)
    • nas linhas dessa junção (a,pa), exibe-se o nome da atividade (a.nom) para a pessoa p2 (pa.personne.nom='p2').
  • linhas 16-21: faz-se o mesmo que anteriormente, mas com a ajuda da relação OneToMany p2.activites da pessoa p2. A consulta JPQL será gerada por JPA. Aqui fica evidente a utilidade da relação inversa OneToMany: ela 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. Teste5

Este teste é o seguinte:


// recuperação de pessoas que realizam uma determinada atividade
    public static void test5() {
        // contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // solicita-se as atividades 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);
        }
        // passa-se pela relação inversa 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());
        }
        // fim da transação
        tx.commit();
    }
  • O teste 6 exibe as pessoas que realizam a atividade act3. O procedimento é semelhante ao do teste 6. Deixamos a cargo do leitor estabelecer a relação entre os dois códigos.

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 é indispensável e sempre pode ser substituída por uma consulta JPQL.

Agora estamos utilizando uma implementação JPA / Toplink:

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

Os códigos Java são idênticos aos do projeto Hibernate anterior, com algumas pequenas diferenças que abordaremos a seguir. O ambiente (bibliotecas – persistence.xml – SGBD – pastas conf, ddl – script ant) é o mesmo estudado no parágrafo 2.1.15.2. O projeto Eclipse está disponível como [3] na pasta de exemplos [4]. Vamos importá-lo.

O arquivo <persistence.xml> [2] é modificado em um ponto, o das entidades declaradas:


        <!-- classes persistentes -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
<class>entites.PersonneActivite</class>
  • linhas 2-5: as quatro entidades gerenciadas

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

No [1], a exibição no console; no [2], as tabelas [jpa07_tl] geradas; no [3], os scripts SQL gerados. Seu conteúdo é o seguinte:

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 de [Main] ocorre sem erros.

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

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

2.6.1. O esquema do banco de dados

  • em [1], o banco de dados MySQL5 – em [2]: a tabela [personne] – para [3]: a tabela [adresse] associada – para [4]: a tabela [activite] de atividades – em [5]: a tabela de junção [personne_activite] que faz a ligação entre pessoas e atividades.

2.6.2. Os objetos @Entity que representam o banco de dados

As tabelas anteriores serão representadas pelas seguintes @Entity:

  • a @Entity Personne representará a tabela [personne]
  • o @Entity Adresse representará a tabela [adresse]
  • a @Entity Activite representará a tabela [activite]
  • a tabela [personne_activite] não é mais representada por uma @Entity

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

  • uma relação um-para-um liga a entidade Personne à entidade Adresse: uma pessoa p possui um endereço a. A entidade Personne, que possui a chave estrangeira, terá a relação principal, enquanto a entidade Adresse terá a relação inversa.
  • Uma relação muitos-a-muitos liga as entidades Personne e Activite: uma pessoa exerce várias atividades e uma atividade é exercida por várias pessoas. Essa relação será representada por uma anotação @ManyToMany em cada uma das duas entidades, sendo que uma delas é declarada como inversa da outra.

A @Entity Personne é 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;

    // relação principal Pessoa (one) -> Endereço (one)
    // implementada pela chave estrangeira Pessoa (adresse_id) -> Endereço
    // cascata de inserção Pessoa -> inserção Endereço
    // cascata de atualização Pessoa -> atualização Endereço
    // eliminação em cascata de Pessoa -> eliminação de Endereço
    // uma Pessoa deve ter 1 Endereço (nullable=false)
    // 1 Endereço pertence a apenas 1 Pessoa (único=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relação Pessoa (muitas) -> Atividade (muitas) por meio de uma tabela de junção personne_activite
    // personne_activite(PERSONNE_ID) é uma chave estrangeira na tabela Pessoa (id)
    // personne_activite(ACTIVITE_ID) é uma chave estrangeira na tabela Atividade (id)
    // cascade=CascadeType.PERSIST: a persistência de uma pessoa implica a persistência de suas atividades
    @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>();

    // construtores
    public Personne() {
    }

Comentamos apenas a relação @ManyToMany das linhas 46-48, que conecta a @Entity Personne à @Entity Activite:

  • linha 48: uma pessoa possui atividades. O campo `activites` representará essas atividades. Na versão anterior, o tipo dos elementos do conjunto `activites` era `PersonneActivite`. Aqui, é `Activite`. Assim, acessa-se diretamente às atividades de uma pessoa, enquanto que na versão anterior era necessário passar pela entidade intermediária PersonneActivite.
  • linha 46: a relação que liga a @Entity Personne que estamos analisando à @Entity Activite do conjunto activites da linha 48 é do tipo muitos-para-muitos (ManyToMany):
    • uma pessoa (One) tem várias atividades (Many)
    • uma atividade (One) é praticada por várias pessoas (Many)
    • No final, as @Entity Personne e Activite estão ligadas por uma relação ManyToMany. Assim como na relação OneToOne, há simetria entre as entidades nessa relação. É possível escolher livremente qual @Entity terá a relação principal e qual terá a relação inversa. Aqui, decidimos que a @Entity Personne terá a relação principal.
    • Como vimos no exemplo anterior, a relação @ManyToMany requer uma tabela de junção. Enquanto anteriormente havíamos definido essa tabela por meio de uma @Entity, aqui a tabela de junção é definida por meio da anotação @JoinTable na linha 47.
      • O atributo name atribui um nome à tabela.
      • A tabela de junção é constituída pelas chaves estrangeiras nas tabelas que ela une. Aqui, há duas chaves estrangeiras: uma na tabela [personne] e outra na tabela [activite]. Essas colunas de chave estrangeira são definidas pelos atributos joinColumns e inverseJoinColumns.
      • A anotação @JoinColumn do atributo joinColumns define a chave estrangeira na tabela da @Entity que mantém a relação principal @ManyToMany, neste caso, a tabela [personne]. Essa coluna de chave estrangeira se chamará PERSONNE_ID.
      • A anotação @JoinColumn do atributo inverseJoinColumns define a chave estrangeira na tabela da @Entity que mantém a relação inversa @ManyToMany, neste caso, a tabela [activite]. Essa coluna de chave estrangeira se chamará ACTIVITE_ID.

A @Entity Adresse é a seguinte:


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

    // campos
    @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, inversa da relação @OneToOne, aponta para a @Entity Personne (linhas 37-38 de Personne).

A @Entity Activite é a seguinte


@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {

    // campos
    @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;

    // relação inversa Atividade -> Pessoa
    @ManyToMany(mappedBy = "activites")
    private Set<Personne> personnes = new HashSet<Personne>();
...
  • linhas 20-21: a relação muitos-para-muitos que vincula a @Entity Activite à @Entity Personne. Essa relação já foi definida na @Entity Personne. Portanto, basta aqui indicar que a relação é inversa (mappedBy) à relação @ManyToMany existente no campo “activites” (mappedBy = “activites”) da @Entidade Personne.
  • Vale lembrar que uma relação inversa é sempre opcional. Aqui, nós a utilizamos para obter as pessoas que praticam a atividade atual. É o conjunto Set<Pessoa> pessoas que permitirá obtê-las. O modo de carregamento das dependências Personne da @Entity Activite não está especificado. Também não o havíamos especificado no exemplo anterior. Por padrão, esse modo é fetch=FetchType.LAZY.

Concluímos a descrição das entidades do banco de dados. Ela foi mais simples do que no caso em que a tabela de junção [personne_activite] é definida explicitamente. Essa solução mais simples pode apresentar desvantagens com o passar do tempo: ela não permite adicionar colunas à tabela de junção. No entanto, isso pode se tornar necessário para atender a novas necessidades, por exemplo, adicionar à tabela [personne_activite] uma coluna indicando a data de inscrição da pessoa na atividade.

2.6.3. O projeto Eclipse / Hibernate

A implementação JPA utilizada aqui é a do Hibernate. O projeto Eclipse dos testes é o seguinte:

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

2.6.4. Geração do arquivo DDL do banco de dados

Seguindo as instruções do parágrafo 2.1.7, o DDL obtido 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 faremos muitos comentários sobre a classe [InitDB], que é idêntica à versão anterior e produz os mesmos resultados. Vamos apenas nos deter no código a seguir, que exibe a junção entre Personne e Activite:


        // exibição de pessoas/atividades
        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 instrução JPQL que realiza a junção. O resultado da instrução select retorna os identificadores das entidades Personne e Activite, ligadas entre si pela tabela de junção. A lista retornada pelo select é formada por linhas que contêm dois objetos do tipo Long. Para percorrer essa lista, a linha 3 solicita um objeto Iterator da lista.
  • linhas 4-7: com a ajuda do objeto do tipo Iterator anterior, percorre-se a lista.
    • linha 5: cada elemento da lista é uma matriz contendo uma linha resultante do select
    • linha 6: recuperam-se os elementos da linha atual resultante do select, realizando as alterações de tipo adequadas.

O resultado do [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] encadeia uma série de testes, dos quais analisaremos alguns.

2.6.6.1. Teste 3

Este teste é o seguinte:


// exclusão da atividade act1
    public static void test3() {
        // contexto de persistência
        EntityManager em = getEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // exclusão da atividade act1 de p2
        p2.getActivites().remove(act1);
        // remoção da atividade act1 do contexto de persistência
        em.remove(act1);
        // fim das transações
        tx.commit();
        // exibição das novas tabelas
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • linha 11: a atividade act1 é removida do contexto de persistência
  • linha 9: a atividade act1 faz parte das atividades da única pessoa que permanece no contexto, a pessoa p2. A linha 9 retira a atividade act1 das atividades da pessoa p2. Fazemos isso para manter a coerência do contexto de persistência, pois o conservamos para as etapas seguintes.

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, presente na linha 26 de test2, desapareceu das atividades de test3 (linhas 40-41)
  • a pessoa p2 tinha, em test2, a atividade act1 (linha 33). Após a conclusão de test3, ela não a possui mais (linha 47)

2.6.6.2. Teste 6

Este teste é o seguinte:


// alteração das atividades de uma pessoa
    public static void test6() {
        // contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // recuperação da pessoa p2
        p2 = em.find(Personne.class, p2.getId());
        // recupera-se a atividade act2
        act2 = em.find(Activite.class, act2.getId());
        // p2 agora exerce apenas a atividade act2
        p2.getActivites().clear();
        p2.getActivites().add(act2);
        // fim da transação
        tx.commit();
        // exibindo as novas tabelas
        dumpPersonne();
        dumpActivite();
        dumpPersonne_Activite();
    }
  • linha 4: utiliza-se um contexto de persistência novo e vazio
  • linha 9: a pessoa p2 é trazida do banco de dados para o contexto de persistência
  • linha 11: a atividade act2 é transferida do banco de dados para o contexto de persistência
  • linha 13: as atividades da pessoa p2 (act3) são transferidas do banco de dados para o contexto (fetchType.LAZY). É a chamada [getActivites] que provoca esse carregamento. As atividades de p2 são removidas. Não se trata de uma remoção real de atividades (remove), mas de uma modificação no status da pessoa p2. Ela não exerce mais atividades.
  • linha 14: adiciona-se à pessoa p2 a atividade act2. No final, o conjunto de novas atividades da pessoa p2 é o conjunto {act2}.
  • linha 16: fim da transação. A sincronização irá examinar os objetos do contexto (p2, act2, act3) e detectará que o estado de p2 mudou. As ordens SQL, que refletem essa alteração no banco de dados, serão executadas.
  • linhas 18-20: exibimos todas as tabelas

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]
  • ao final do teste 4, a pessoa p2 exercia a atividade act3 (linha 3).
  • Após o teste 6 (linha 19), a pessoa p2 não exerce mais a atividade act3 (linha 3) e passa a exercer a atividade act2.

Agora utilizamos uma implementação JPA / Toplink:

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

O arquivo <persistence.xml> [2] foi alterado em um ponto, o das entidades declaradas:


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

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

No [1], a exibição no console; no [2], as tabelas geradas no [jpa07_tl]; no [3], os scripts gerados no SQL. 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 de [Main] ocorreu sem erros.

2.6.8. O projeto Eclipse / Hibernate 2

Criamos um projeto Eclipse a partir do anterior, copiando-o:

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

Alteramos a relação que liga Personne a Activité da seguinte maneira:

Pessoa


    // relação Pessoa (muitas) -> Atividade (muitas) por meio de uma tabela de junção personne_activite
    // personne_activite(PERSONNE_ID) é uma chave estrangeira em Pessoa (id)
    // personne_activite(ACTIVITE_ID) é uma chave estrangeira na tabela Atividade (id)
    // mais cascata nas atividades
    // @ManyToMany(cascata={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 principal @ManyToMany não possui mais a cascata de persistência Pessoa -> Atividade (ver versão anterior, linha 5)

Atividade


    // sem relação inversa com Pessoa
    // @ManyToMany(mappedBy = "atividades")
// private Set<Pessoa> pessoas = new HashSet<Pessoa>();
  • linhas 2-3: a relação inversa @ManyToMany Atividade -> Pessoa foi removida

Pretendemos demonstrar que os atributos removidos (cascata e relação inversa) não são indispensáveis. A primeira alteração trazida por essa nova configuração encontra-se em [InitDB]:


        // associações pessoas <--> atividades
        p1.getActivites().add(act1);
        p1.getActivites().add(act2);
        p2.getActivites().add(act1);
        p2.getActivites().add(act3);
        // persistência das atividades
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // persistência das pessoas
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // e do endereço a4 não vinculado a uma pessoa
em.persist(adr4);
  • linhas 7-9: somos obrigados a colocar explicitamente as atividades act1 a act3 no contexto de persistência. Quando a cascata de persistência Pessoa -> Atividade existia, as linhas 11-13 persistiam tanto as pessoas de p1 a p3 quanto as atividades dessas pessoas de act1 a act3.

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


    // recuperação de pessoas que realizam uma determinada atividade
    public static void test5() {
        // contexto de persistência
        EntityManager em = getNewEntityManager();
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // solicitação das atividades 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);
        }
        // fim da transação
        tx.commit();
}
  • linhas 9-12: a consulta JPQL que obtém as pessoas que praticam a atividade act3
  • na versão anterior, o mesmo resultado também era obtido por meio da relação inversa Atividade -> Pessoa, agora removida:

        // passa-se pela relação inversa de 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 criando um projeto Eclipse a partir do projeto Eclipse / Toplink anterior, por meio de cópia:

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

Os códigos Java são idênticos aos da versão Hibernate.

2.7. Exemplo 7: usar consultas nomeadas

Concluímos esta longa apresentação das entidades JPA, iniciada no parágrafo 2, com um último exemplo que mostra o uso de consultas JPQL externalizadas em um arquivo de configuração. Este exemplo tem origem na fonte a seguir:

[ref2]: “Getting started With JPA in Spring 2.0”, de Mark Fisher, no endereço

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

2.7.1. O banco de dados de exemplo

O banco de dados é o seguinte:

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

2.7.2. Os objetos @Entity que representam o banco de dados

As tabelas anteriores serão representadas pelas seguintes @Entity:

  • a @Entity Restaurant representará a tabela [restaurant]
  • o @Entity Adresse representará a tabela [adresse]
  • a @Entity Plat representará a tabela [plat]

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

  • uma relação um-para-um liga a entidade Restaurant à entidade Adresse: um restaurante r possui um endereço a. A entidade Restaurant, que possui a chave estrangeira, terá a relação principal. A entidade Adresse não terá uma relação inversa.
  • Uma relação muitos-a-muitos liga as entidades Restaurant e Plat: um restaurante serve vários pratos e um mesmo prato pode ser servido por vários restaurantes. Essa relação será representada por uma anotação @ManyToMany na entidade Restaurant. A entidade Plat 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>();

    // construtores
    public Restaurant() {

    }

    public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
    }

    // getters e 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 que a entidade Restaurant mantém com a entidade Adresse. Todas as operações de persistência em um restaurante são propagadas para seu endereço.
  • linha 20: a relação que vincula a @Entity Restaurant à @Entity Plat do conjunto plats da 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)
    • no final, as @Entity Restaurant e Plat estão ligadas por uma relação ManyToMany. Decidimos que a @Entity Restaurant terá a relação principal e que a @Entity Plat não terá uma relação inversa.
    • A relação @ManyToMany requer uma tabela de junção. Ela é definida por meio da anotação @JoinTable na linha 47.
      • O atributo name atribui um nome à tabela.
      • A tabela de junção é constituída pelas chaves estrangeiras das tabelas que ela une. Aqui, há duas chaves estrangeiras: uma na tabela [restaurant] e outra na tabela [plat]. Essas 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 principal @ManyToMany, neste caso, a tabela [restaurant]. O atributo joinColumns está ausente aqui. JPA tem um valor padrão neste caso: [table]_[clé_primaire_de_table], neste caso, [jpa10_hb_restaurant_id].
      • A anotação @JoinColumn do atributo inverseJoinColumns define a chave estrangeira na tabela da @Entity que mantém a relação inversa @ManyToMany, neste caso, a tabela [plat]. Essa coluna de chave estrangeira se chamará plat_id.

A @Entity Adresse é 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 e setters
 ...
 
  // construtores
  public Adresse(int streetNumber, String streetName){
...
  }
  
  public Adresse(){
    
  }
  
  // toString
  public String toString(){
    return "A["+getNumeroRue()+","+getNomRue()+"]";
  }
}
  • A @Entity Adresse é uma entidade sem relação direta com as outras entidades. Ela só pode ser persistida por meio de uma entidade Restaurant.
  • Um endereço é definido por um nome de rua (linha 16) e um número na rua (linha 13).

A @Entity Plat é 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;

    // construtores
    public Plat() {

    }

    public Plat(String name, boolean vegetarian) {
...
    }

    // getters e setters
...

    // toString
    public String toString() {
        return "E[" + getNom() + "," + isVegetarien() + "]";
    }

}
  • A @Entity Plat é uma entidade sem relação direta com as outras entidades. Ela só pode ser persistida por meio da entidade Restaurant.
  • Um prato é definido por um nome (linha 12) e um tipo (vegetariano ou não) (linha 14).

2.7.3. O projeto Eclipse / Hibernate

A implementação JPA utilizada aqui é a do Hibernate. O projeto Eclipse dos testes é o seguinte:

Em [1], o projeto Eclipse; em [2], os códigos Java; e em JPA, a configuração da camada. Observe-se a presença de um arquivo [orm.xml] que ainda não foi encontrado. O projeto está presente em [3] na pasta de exemplos [4]. Vamos importá-lo.

2.7.4. Geração do arquivo DDL a partir do banco de dados

Seguindo as instruções do parágrafo 2.1.7, o arquivo DDL obtido para os arquivos SGBD e 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 [adresse]
  • linhas 28-33: a tabela [plat]
  • linhas 35-40: a tabela [restaurant]
  • linhas 42-46: a tabela de junção [restaurant_plat]. Observe-se a chave composta (linha 45)
  • linhas 48-52: a chave estrangeira da tabela [restaurant] para a tabela [adresse]
  • linhas 54-58: a chave estrangeira da tabela [restaurant_plat] para a tabela [plat]
  • linhas 60-64: a chave estrangeira da tabela [restaurant_plat] para a tabela [restaurant]

Essa tabela DDL corresponde ao esquema já apresentado:

Na perspectiva SQL do Explorer, o banco de dados se apresenta da seguinte forma:

  • em [1]: as 4 tabelas do banco de dados
  • em [2]: os endereços
  • em [3]: os pratos
  • em [4]: os restaurantes. [adresse_id] faz referência aos endereços de [2].
  • em [5]: a tabela de junção [restaurant,plat]. [jpa10_hb_restaurant_id] faz referência aos restaurantes de [4] e [plat_id] aos pratos de [3]. Assim, [1,1] significa que o restaurante “Burger Barn” serve o prato “CheeseBurger”.

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

2.7.5. Consultas JPQL com um console Hibernate

Criamos um console Hibernate vinculado ao projeto Eclipse anterior. Seguiremos o procedimento já exposto duas vezes, notadamente no parágrafo 2.1.12.

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

Apresentamos agora uma série de consultas JPQL. O leitor é convidado a executá-las e a descobrir a ordem SQL gerada pelo Hibernate para executá-las.

Obter todos os restaurantes com seus pratos:

Obter os restaurantes que servem pelo menos um prato vegetariano:

Obter os nomes dos restaurantes que servem apenas pratos vegetarianos:

Obter os restaurantes que servem hambúrgueres:

2.7.6. QueryDB

Agora vamos nos concentrar no programa [QueryDB] do projeto Eclipse, que:

  • preenche o banco de dados
  • e envia a partir dela uma série de consultas JPQL. Essas consultas são registradas no arquivo [META-INF/orm.xml] do projeto Eclipse:

O arquivo [orm.xml] pode ser usado para configurar a camada JPA em vez das anotações Java. Isso proporciona flexibilidade na configuração da camada JPA. É possível modificá-la sem recompilar os códigos Java. É possível utilizar os dois métodos simultaneamente: anotações Java e o arquivo [orm.xml]. A configuração JPA é feita primeiro com as anotações Java e, em seguida, com o arquivo [orm.xml]. Portanto, se quisermos alterar uma configuração feita por meio de uma anotação Java sem recompilar, basta inserir essa configuração no arquivo [orm.xml]. É ela que prevalecerá.

Em nosso exemplo, o arquivo [orm.xml] é usado para registrar textos de consultas do JPQL. 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 arquivo [orm.xml] é <entity-mappings> (linha 2).
  • linhas 5-7: as consultas nomeadas JPQL são objeto das tags <named-query name= "... ">texto</namedquery>.
    • O atributo name da tag é o nome da consulta.
    • O conteúdo texte da tag é o texto da consulta.

QueryDB executará as consultas anteriores. Seu código é o seguinte:


package tests;

...
public class QueryDB {

    // Contexto de persistência
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = emf.createEntityManager();

    public static void main(String[] args) {
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // excluir os elementos da tabela [restaurant]
        em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
        // excluir os elementos da tabela [plat]
        em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
        // criação de objetos Address
        Adresse adr1 = new Adresse(10, "Main Street");
        Adresse adr2 = new Adresse(20, "Main Street");
        Adresse adr3 = new Adresse(123, "Dover Street");
        // Criação de objetos “Entrada”
        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);
        // criação de objetos Restaurant
        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);
        // persistência dos objetos Restaurant (e dos demais objetos em cascata)
        em.persist(restaurant1);
        em.persist(restaurant2);
        em.persist(restaurant3);
        // fim da transação
        tx.commit();
        // dump da base
        dumpDataBase();
        // fim EntityManager
        em.close();
        // fim de EntityManagerFactory
        emf.close();
    }

    // exibição do conteúdo do banco de dados
    @SuppressWarnings("unchecked")
    private static void dumpDataBase() {
        // teste2
        log("données de la base");
        // início da transação
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // exibições de restaurantes
        log("[restaurants]");
        for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
            System.out.println(restaurant);
        }
        // exibições de endereços
        log("[adresses]");
        for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
            System.out.println(adresse);
        }
        // visualizações de pratos
        log("[plats]");
        for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
            System.out.println(plat);
        }
        // visualizações de links entre restaurantes <--> pratos
        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);
        }
        // consulta
        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);
        }
        // consulta
        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);
        }
        // consulta
        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]);
        }
        // consulta
        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);
        }
        // fim da transação
        tx.commit();
    }

    // registros
    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 a cargo do leitor a tarefa de estabelecer a relação entre o código e os resultados. Para isso, recomendamos que ele execute as consultas JPQL no console do Hibernate e examine o código SQL correspondente.

O leitor interessado encontrará, nos exemplos disponíveis para download com este tutorial, o projeto anterior implementado com o Toplink:

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

O arquivo <persistence.xml> [2] declara as entidades gerenciadas:


        <!--  provedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
            <!-- classes persistentes -->
        <class>entites.Restaurant</class>
        <class>entites.Adresse</class>
        <class>entites.Plat</class>

...
  • linhas 4-6: as entidades gerenciadas

As consultas JPQL registradas em [orm.xml] são executadas corretamente pelo Toplink. Para isso, no projeto anterior, tomamos o cuidado de não utilizar consultas HQL (Hibernate Query Language), que, na verdade, é um superconjunto de JPQL e cujas sintaxes não são aceitas pelo JPQL.

2.8. Conclusion

Concluímos aqui nosso estudo sobre as entidades JPA. Foi um processo demorado e, ainda assim, alguns aspectos importantes (para o desenvolvedor avançado) não foram abordados. Mais uma vez, é recomendável ler um livro de referência como o que foi utilizado neste tutorial:

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