Como usar a JPA com o App Engine

A Java Persistence API (JPA) é uma interface padrão para acessar bancos de dados em Java e fornece um mapeamento automático entre classes Java e tabelas de banco de dados. Há um plug-in de código aberto disponível para usar a JPA com o Datastore. Nesta página, fornecemos informações sobre como começar a usar esse plug-in.

Aviso: acreditamos que a maioria dos desenvolvedores terá uma experiência melhor com a API Datastore de nível inferior ou uma das APIs de código aberto desenvolvidas especificamente para o Datastore, como a Objectify (em inglês). A JPA foi projetada para uso com bancos de dados relacionais tradicionais e, portanto, não pode representar explicitamente alguns dos aspectos do Datastore que o diferem dos bancos de dados relacionais, como grupos de entidades e consultas de ancestral. Isso pode causar problemas sutis que são difíceis de entender e corrigir.

O Java SDK do App Engine inclui a versão 2.x do plug-in DataNucleus para Datastore. Esse plug-in corresponde à versão 3.0 da DataNucleus Access Platform, que permite usar o Datastore do App Engine por meio da JPA 2.0.

Para mais informações sobre JPA, consulte a documentação do Access Platform 3.0. Em especial, confira a Documentação da JPA e da API JPA (todos em inglês).

Aviso: a versão 2.x do plug-in DataNucleus para o App Engine usa o DataNucleus v3.x. O plug-in 2.x não é totalmente compatível com o plug-in 1.x anterior. Se você atualizar para a nova versão, não se esqueça de atualizar e testar o aplicativo.

Ferramentas de criação compatíveis com JPA 2.x e 3.0

Você pode usar o Apache Ant ou Maven para usar a versão 2.x ou 3.0 do plug-in DataNucleus para App Engine:

  • Para usuários do Ant: o SDK inclui uma tarefa Ant que executa a etapa de melhoria. Você precisa copiar os JARs e criar o arquivo de configuração ao definir seu projeto.
  • Para usuários do Maven: é possível melhorar as classes com as seguintes configurações no arquivo pom.xml:
                <plugin>
                    <groupId>org.datanucleus</groupId>
                    <artifactId>maven-datanucleus-plugin</artifactId>
                    <version>3.2.0-m1</version>
                    <configuration>
                        <api>JDO</api>
                        <props>${basedir}/datanucleus.properties</props>
                        <verbose>true</verbose>
                        <enhancerName>ASM</enhancerName>
                    </configuration>
                    <executions>
                        <execution>
                            <phase>process-classes</phase>
                            <goals>
                                <goal>enhance</goal>
                            </goals>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>org.datanucleus</groupId>
                            <artifactId>datanucleus-api-jdo</artifactId>
                            <version>3.1.3</version>
                        </dependency>
                    </dependencies>
                </plugin>

Como migrar para a versão 2.x do plug-in DataNucleus

Nesta seção, fornecemos instruções para o upgrade do app para que ele use a versão 2.x do plug-in DataNucleus para App Engine, que corresponde ao DataNucleus Access Platform 3.0 e à JPA 2.0. O plug-in 2.x não é totalmente compatível com a versão 1.x e pode ser alterado sem aviso. Se você fizer o upgrade, certifique-se de atualizar e testar o código do aplicativo.

Novos comportamentos padrão

A versão 2.x do plug-in DataNucleus do App Engine tem alguns padrões diferentes da versão 1.x anterior:

  • O "provedor de persistência" JPA agora é org.datanucleus.api.jpa.PersistenceProviderImpl.
  • O armazenamento em cache de nível 2 é ativado por padrão. Para conseguir o comportamento padrão anterior, defina a propriedade de persistência datanucleus.cache.level2.type como none. Como alternativa, inclua o plug-in datanucleus-cache no classpath e defina a propriedade de persistência datanucleus.cache.level2.type como javax.cache, assim será possível usar o Memcache para armazenamento em cache nível 2.
  • O Datastore IdentifierFactory agora é o padrão de datanucleus2. Para conseguir o comportamento anterior, defina a propriedade de persistência datanucleus.identifierFactory como datanucleus1.
  • Chamadas não transacionais para EntityManager.persist(), EntityManager.merge() e EntityManager.remove() agora são executadas de maneira atômica. Anteriormente, a execução ocorria na transação seguinte ou mediante EntityManager.close().
  • A JPA tem retainValues ativado, ou seja, os valores dos campos carregados são mantidos em objetos após um commit.
  • javax.persistence.query.chunkSize não é mais usado. Use datanucleus.query.fetchSize vez dele.
  • Agora não há mais uma exceção na alocação de EMF duplicado. Se a propriedade de persistência datanucleus.singletonEMFForName estiver definida como verdadeiro, o EMF de singleton atualmente alocado para esse nome será retornado.
  • Agora, os relacionamentos sem proprietários são compatíveis.
  • O Datastore Identity agora é compatível.

Para uma lista completa de novos recursos, consulte as notas da versão.

Alterações nos arquivos de configuração

Para atualizar o aplicativo para usar a versão 2.0 do plug-in DataNucleus para App Engine, você precisa alterar algumas configurações no build.xml e no persistence.xml. Se você estiver configurando um novo aplicativo e quiser usar a versão mais recente do plug-in DataNucleus, acesse Como configurar a JPA 2.0.

Aviso. Depois de atualizar a configuração, você precisa testar o código do aplicativo para garantir a compatibilidade com versões anteriores.

No build.xml

O destino de copyjars precisa mudar para acomodar o DataNucleus 2.x:

  1. O destino de copyjars mudou. Atualize esta seção:
      <target name="copyjars"
          description="Copies the App Engine JARs to the WAR.">
        <mkdir dir="war/WEB-INF/lib" />
        <copy
            todir="war/WEB-INF/lib"
            flatten="true">
          <fileset dir="${sdk.dir}/lib/user">
            <include name="**/*.jar" />
          </fileset>
        </copy>
      </target>

    para:
      <target name="copyjars"
          description="Copies the App Engine JARs to the WAR.">
        <mkdir dir="war/WEB-INF/lib" />
        <copy
            todir="war/WEB-INF/lib"
            flatten="true">
          <fileset dir="${sdk.dir}/lib/user">
            <include name="**/appengine-api-1.0-sdk*.jar" />
          </fileset>
          <fileset dir="${sdk.dir}/lib/opt/user">
            <include name="appengine-api-labs/v1/*.jar" />
            <include name="jsr107/v1/*.jar" />
            <include name="datanucleus/v2/*.jar" />
          </fileset>
        </copy>
      </target>
  2. O destino de datanucleusenhance mudou. Atualize esta seção:
      <target name="datanucleusenhance" depends="compile"
          description="Performs enhancement on compiled data classes.">
        <enhance_war war="war" />
      </target>

    para:
      <target name="datanucleusenhance" depends="compile"
          description="Performs enhancement on compiled data classes.">
          <enhance_war war="war">
                  <args>
                  <arg value="-enhancerVersion"/>
                  <arg value="v2"/>
              </args>
          </enhance_war>
      </target>

No persistence.xml

O destino de <provider> mudou. Atualize esta seção:

        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>

para:

        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>

Como configurar a JPA 2.0

Para usar a JPA para acessar o armazenamento de dados, um aplicativo do App Engine precisa que:

  • a JPA e os arquivos JAR do armazenamento de dados estejam no diretório war/WEB-INF/lib/ do aplicativo;
  • um arquivo de configuração denominado persistence.xml esteja no diretório war/WEB-INF/classes/META-INF/ do aplicativo, com a configuração que instrui a JPA a usar o armazenamento de dados do App Engine;
  • o processo de criação do projeto precisa executar uma etapa de "melhoria" pós-compilação nas classes de dados compiladas para associá-las à implementação da JPA.

Como copiar os JARs

Os JARs da JPA e do armazenamento de dados estão incluídos no Java SDK do App Engine. Você pode encontrá-los no diretório appengine-java-sdk/lib/opt/user/datanucleus/v2/.

Copie os JARs para o diretório war/WEB-INF/lib/ do aplicativo.

Verifique se appengine-api.jar também está no diretório war/WEB-INF/lib/. Pode ser que você já tenha copiado esse arquivo ao criar o projeto. O plug-in DataNucleus do Google App Engine usa esse JAR para acessar o armazenamento de dados.

Como criar o arquivo persistence.xml

A interface JPA precisa de um arquivo de configuração denominado persistence.xml no diretório war/WEB-INF/classes/META-INF/ do aplicativo. Você pode criar esse arquivo diretamente nesse local ou pode fazer com que o processo de criação copie o arquivo de um diretório de origem.

Crie o arquivo com o seguinte conteúdo:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
            <property name="datanucleus.singletonEMFForName" value="true"/>
        </properties>

    </persistence-unit>

</persistence>

Política de leitura e duração máxima da chamada do Datastore

Conforme descrito na página Consultas do Datastore, é possível definir a política de leitura (consistência forte ou eventual) e a duração máxima da chamada ao armazenamento de dados para um EntityManagerFactory no arquivo persistence.xml. Essas configurações estão contidas no elemento <persistence-unit>. Todas as chamadas feitas com uma determinada instância EntityManager usam a configuração selecionada quando o gerenciador foi criado por EntityManagerFactory. Você também pode modificar essas opções com uma Query individual (descrita abaixo).

Para definir a política de leitura, inclua uma propriedade denominada datanucleus.appengine.datastoreReadConsistency. Os valores possíveis dessa propriedade são EVENTUAL (para leituras com consistência eventual) e STRONG (para leituras com consistência forte). Se não for especificado, o padrão será STRONG.

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

Você pode definir durações máximas de chamadas ao armazenamento de dados separadas para leituras e gravações. Para leituras, use a propriedade padrão da JPA, javax.persistence.query.timeout. Para gravações, use datanucleus.datastoreWriteTimeout. O valor é uma quantidade de tempo em milissegundos.

            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />

Se você quiser usar transações entre grupos (XG, na sigla em inglês), adicione a seguinte propriedade:

            <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

Você pode ter vários elementos <persistence-unit> no mesmo arquivo persistence.xml usando atributos name diferentes para usar instâncias EntityManager com configurações distintas no mesmo aplicativo. Por exemplo, o arquivo persistence.xml a seguir estabelece dois conjuntos de configurações, um denominado "transactions-optional" e outro denominado "eventual-reads-short-deadlines":

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="eventual-reads-short-deadlines">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />
            <property name="datanucleus.singletonEMFForName" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Para informações sobre como criar um EntityManager com um conjunto de configurações nomeado, consulte Como conseguir uma instância do EntityManager, abaixo.

Você pode modificar a política de leitura e a duração máxima de chamadas de um objeto Query individual. Para substituir a política de leitura de uma Query, chame o respectivo método setHint() da seguinte maneira:

        Query q = em.createQuery("select from " + Book.class.getName());
        q.setHint("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

Como descrito acima, os valores possíveis são "EVENTUAL" e "STRONG".

Para modificar o tempo limite de leitura, chame setHint() da seguinte maneira:

        q.setHint("javax.persistence.query.timeout", 3000);

Não é possível modificar a configuração dessas opções ao buscar entidades por chave.

Como aprimorar classes de dados

A implementação da JPA com o DataNucleus usa uma etapa de "aprimoramento" pós-compilação no processo de criação para associar classes de dados à implementação da JPA.

Para realizar a etapa de aprimoramento em classes compiladas a partir da linha de comando, use o seguinte comando:

java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer
class-files

O classpath precisa conter os JARs datanucleus-core-*.jar, datanucleus-jpa-*, datanucleus-enhancer-*.jar, asm-*.jar e geronimo-jpa-*.jar, em que * é o número de versão apropriado de cada JAR, do diretório appengine-java-sdk/lib/tools/, bem como todas as respectivas classes de dados.

Para mais informações sobre o otimizador de bytecode do DataNucleus, consulte a documentação do DataNucleus.

Como receber uma instância de EntityManager

Um aplicativo interage com a JPA usando uma instância da classe EntityManager. Para recebê-la, instancie e chame um método em uma instância da classe EntityManagerFactory. A configuração da JPA (identificada pelo nome "transactions-optional") é usada pela fábrica para criar instâncias de EntityManager.

Como uma instância de EntityManagerFactory demora para ser inicializada, convém reutilizar uma única instância tanto quanto possível. Uma maneira fácil de fazer isso é criar uma classe wrapper de singleton com uma instância estática, desta forma:

EMF.java

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

Dica: "transactions-optional" refere-se ao nome da configuração definida no arquivo persistence.xml. Se o aplicativo usar vários conjuntos de configuração, você precisará estender esse código para chamar Persistence.createEntityManagerFactory() como quiser. O código armazena em cache uma instância singleton de cada EntityManagerFactory.

O aplicativo usa a instância de fábrica para criar uma instância de EntityManager para cada solicitação que acessa o armazenamento de dados.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import EMF;

// ...
    EntityManager em = EMF.get().createEntityManager();

Use EntityManager para armazenar, atualizar e excluir objetos de dados e executar consultas ao armazenamento de dados.

Quando terminar de usar a instância de EntityManager, chame o método close() dela. É um erro usar a instância de EntityManager depois de chamar o método close().

    try {
        // ... do stuff with em ...
    } finally {
        em.close();
    }

Anotações de classe e campo

Cada objeto salvo pela JPA torna-se uma entidade no armazenamento de dados do App Engine. O tipo da entidade é derivado do nome simples da classe (sem o nome do pacote). Cada campo persistente da classe representa uma propriedade da entidade, e o nome da propriedade é igual ao nome do campo (com letras maiúsculas e minúsculas preservadas).

Para declarar que uma classe Java pode ser armazenada e recuperada do armazenamento de dados com a JPA, atribua uma anotação @Entity a ela. Exemplo:

import javax.persistence.Entity;

@Entity
public class Employee {
    // ...
}

Campos da classe de dados que deverão ser armazenados no armazenamento de dados precisam ser de um tipo persistido por padrão ou declarado explicitamente como persistente. Consulte o site do DataNucleus para ver um gráfico detalhado sobre o comportamento de persistência padrão da JPA. Para declarar explicitamente um campo como persistente, atribua a ele a anotação @Basic:

import java.util.Date;
import javax.persistence.Enumerated;

import com.google.appengine.api.datastore.ShortBlob;

// ...
    @Basic
    private ShortBlob data;

O tipo de um campo pode ser qualquer um destes:

  • um dos tipos principais compatíveis com o armazenamento de dados
  • uma coleção (como java.util.List<...>) de valores de um tipo principal de armazenamento de dados
  • uma instância ou coleção de instâncias de uma classe @Entity
  • uma classe incorporada, armazenada como propriedades na entidade

Uma classe de dados precisa ter um construtor padrão público ou protegido e um campo dedicado a armazenar a chave principal da entidade correspondente do armazenamento de dados. Você pode escolher entre quatro tipos diferentes de campos de chave. Cada um deles usa um tipo de valor e anotações diferentes. Consulte Como criar dados: chaves para mais informações. O campo de chave mais simples é um valor inteiro longo preenchido automaticamente pela JPA com um valor exclusivo para todas as outras instâncias da classe quando o objeto é salvo no armazenamento de dados pela primeira vez. Nas chaves de valor inteiro longo, são usadas uma anotação @Id e uma anotação @GeneratedValue(strategy = GenerationType.IDENTITY):

import com.google.appengine.api.datastore.Key;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

// ...
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

Veja um exemplo de classe de dados:

import com.google.appengine.api.datastore.Key;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String firstName;

    private String lastName;

    private Date hireDate;

    // Accessors for the fields. JPA doesn't use these, but your application
    does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

Herança

A JPA é compatível com a criação de classes de dados que usam herança. Antes de falar sobre como a herança da JPA funciona no App Engine, recomendamos que você leia a documentação do DataNucleus sobre esse assunto e depois volte. Terminou? Tudo bem. A herança da JPA no App Engine funciona conforme descrito na documentação do DataNucleus, com algumas restrições extras. Discutiremos essas restrições e, em seguida, daremos alguns exemplos concretos.

A estratégia de herança "JOINED" permite que você divida os dados de um único objeto em várias "tabelas". Porém, como o armazenamento de dados do App Engine não é compatível com junções, a operação em um objeto de dados com essa estratégia de herança requer uma chamada de procedimento remoto para cada nível de herança. Isso pode ser bastante ineficiente. Portanto, a estratégia de herança "JOINED" não é compatível com as classes de dados.

Já a estratégia de herança "SINGLE_TABLE" permite que você armazene os dados de um objeto em uma única "tabela" associada à classe persistente na raiz da hierarquia de herança. Não há ineficiências inerentes a essa estratégia, mas ela não é compatível atualmente. Podemos rever isso em versões futuras.

Por outro lado, as estratégias "TABLE_PER_CLASS" e "MAPPED_SUPERCLASS" funcionam conforme descrito na documentação do DataNucleus. Vejamos um exemplo:

Worker.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@Entity
@MappedSuperclass
public abstract class Worker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String department;
}

Employee.java

// ... imports ...

@Entity
public class Employee extends Worker {
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@Entity
public class Intern extends Worker {
    private Date internshipEndDate;
}

Nesse exemplo, adicionamos uma anotação @MappedSuperclass à declaração da classe Worker. Ela faz com que a JPA armazene todos os campos persistentes do Worker nas entidades do armazenamento de dados das respectivas subclasses. A entidade de armazenamento de dados criada como resultado da chamada a persist() com uma instância de Employee terá duas propriedades, denominadas "department" e "salary". A entidade de armazenamento de dados criada como resultado da chamada a persist() com uma instância de Intern terá duas propriedades, denominadas "department" e "inernshipEndDate". Não haverá entidades do tipo "Worker" no armazenamento de dados.

Agora, vamos deixar as coisas um pouco mais interessantes. Suponha que, além de Employee e Intern, também queremos uma especialização de Employee que descreva os funcionários que saíram da empresa:

FormerEmployee.java

import java.util.Date;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
// ... imports ...

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class FormerEmployee extends Employee {
    private Date lastDay;
}

Nesse exemplo, adicionamos uma anotação @Inheritance à declaração da classe FormerEmployee, com o atributo strategy definido como InheritanceType.TABLE_PER_CLASS. Isso faz com que a JPA armazene todos os campos persistentes de FormerEmployee e as respectivas superclasses nas entidades de armazenamento de dados correspondentes às instâncias de FormerEmployee. A entidade de armazenamento de dados criada como resultado da chamada a persist() com uma instância de FormerEmployee terá três propriedades, denominadas "department", "salary" e "lastDay". Nunca haverá uma entidade do tipo "Employee" que corresponda a um FormerEmployee. Porém, se você chamar persist() com um objeto que tem um tempo de execução do tipo Employee, você criará uma entidade do tipo "Employee".

Misturar relacionamentos com herança só funciona se os tipos declarados dos campos de relacionamento corresponderem aos tipos de tempo de execução dos objetos que você atribuir a esses campos. Consulte a seção Relações polimórficas para mais informações. Essa seção contém exemplos da JDO, mas os conceitos e restrições são os mesmos para a JPA.

Recursos não compatíveis da JPA 2.0

A implementação do App Engine não é compatível com os seguintes recursos da interface JPA:

  • Relacionamentos proprietários de vários para vários.
  • Consultas "join". Não é possível usar um campo de uma entidade filha em um filtro ao executar uma consulta sobre o tipo da mãe. Observe que você pode testar o campo de relacionamento da mãe diretamente em uma consulta usando uma chave.
  • Consultas de agregação (group by, having, sum, avg, max, min).
  • Consultas polimórficas. Não é possível realizar uma consulta de uma classe para receber instâncias de uma subclasse. Cada classe é representada por um tipo de entidade separado no armazenamento de dados.