Usar a JPA com o App Engine

A API Java Persistence (JPA) é uma interface padrão para aceder a bases de dados em Java, que fornece um mapeamento automático entre classes Java e tabelas de bases de dados. Existe um plug-in de código aberto disponível para usar o JPA com o Datastore, e esta página fornece informações sobre como começar a usá-lo.

Aviso: acreditamos que a maioria dos programadores terá uma melhor experiência com a API Datastore de baixo nível ou uma das APIs de código aberto desenvolvidas especificamente para o Datastore, como o Objectify. O JPA foi concebido para utilização com bases de dados relacionais tradicionais e, por isso, não tem forma de representar explicitamente alguns dos aspetos do Datastore que o tornam diferente das bases de dados relacionais, como grupos de entidades e consultas de antecessores. Isto pode levar a problemas subtis difíceis de compreender e corrigir.

O SDK Java do App Engine inclui a versão 2.x do plug-in DataNucleus para o Datastore. Este plug-in corresponde à versão 3.0 da plataforma de acesso DataNucleus, que lhe permite usar o App Engine Datastore através do JPA 2.0.

Consulte a documentação da plataforma de acesso 3.0 para mais informações sobre a JPA. Em particular, consulte a documentação da JPA.

Aviso: a versão 2.x do plug-in DataNucleus para o App Engine usa o DataNucleus v3.x. O plugin 2.x não é totalmente retrocompatível com o plugin 1.x anterior. Se atualizar para a nova versão, certifique-se de que atualiza e testa a sua aplicação.

Crie ferramentas que suportem JPA 2.x e 3.0

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

  • Para utilizadores do Ant: o SDK inclui uma tarefa do Ant que executa o passo de melhoramento. Tem de copiar os ficheiros JAR e criar o ficheiro de configuração quando configurar o projeto.
  • Para utilizadores do Maven: pode melhorar as classes com as seguintes configurações no ficheiro 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>

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

Esta secção fornece instruções para atualizar a sua app de modo a usar a versão 2.x do plug-in DataNucleus para o App Engine, que corresponde à DataNucleus Access Platform 3.0 e à JPA 2.0. A versão 2.x do plug-in não é totalmente compatível com a versão 1.x e pode ser alterada sem aviso. Se fizer a atualização, certifique-se de que atualiza e testa o código da aplicação.

Novos comportamentos predefinidos

A versão 2.x do plugin do App Engine DataNucleus tem algumas predefinições diferentes das da versão 1.x anterior:

  • O "fornecedor de persistência" da JPA é agora org.datanucleus.api.jpa.PersistenceProviderImpl.
  • A colocação em cache de nível 2 está ativada por predefinição. Para obter o comportamento predefinido anterior, defina a propriedade de persistência datanucleus.cache.level2.type como none. (Em alternativa, inclua o plug-in datanucleus-cache no classpath e defina a propriedade de persistência datanucleus.cache.level2.type como javax.cache para usar o Memcache para o armazenamento em cache de nível 2.
  • O armazenamento de dados IdentifierFactory está agora predefinido para datanucleus2. Para obter o comportamento anterior, defina a propriedade de persistência datanucleus.identifierFactory como datanucleus1.
  • As chamadas não transacionais para EntityManager.persist(), EntityManager.merge() e EntityManager.remove() são agora executadas de forma atómica. (Anteriormente, a execução ocorria na transação seguinte ou no valor de EntityManager.close().
  • O JPA tem a opção retainValues ativada, o que significa que os valores dos campos carregados são retidos nos objetos após uma confirmação.
  • javax.persistence.query.chunkSize já não é usado. Em alternativa, use datanucleus.query.fetchSize.
  • Já não existe uma exceção na atribuição de EMF duplicada. Se tiver a propriedade de persistência datanucleus.singletonEMFForName definida como true, devolve o EMF singleton atualmente atribuído para esse nome.
  • As relações não pertencentes são agora suportadas.
  • A identidade do Datastore é agora suportada.

Para ver uma lista completa das novas funcionalidades, consulte as notas de lançamento.

Alterações aos ficheiros de configuração

Para atualizar a sua app para usar a versão 2.0 do plug-in DataNucleus para o App Engine, tem de alterar algumas definições de configuração em build.xml e persistence.xml. Se estiver a configurar uma nova aplicação e quiser usar a versão mais recente do plug-in DataNucleus, avance para a secção Configurar JPA 2.0.

Aviso! Depois de atualizar a configuração, tem de testar o código da aplicação para garantir a compatibilidade com versões anteriores.

Em build.xml

O alvo copyjars tem de ser alterado para se adequar ao DataNucleus 2.x:

  1. O alvo de copyjars foi alterado. Atualize esta secçã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 alvo de datanucleusenhance foi alterado. Atualize esta secçã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>

Em persistence.xml

O alvo de <provider> foi alterado. Atualize esta secção:

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

para:

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

Configurar o JPA 2.0

Para usar a JPA para aceder ao arquivo de dados, uma app do App Engine precisa do seguinte:

  • Os JARs da JPA e do armazenamento de dados têm de estar no diretório war/WEB-INF/lib/ da app.
  • Tem de existir um ficheiro de configuração denominado persistence.xml no diretório war/WEB-INF/classes/META-INF/ da app, com a configuração que indica ao JPA para usar o datastore do App Engine.
  • O processo de compilação do projeto tem de executar um passo de "melhoramento" pós-compilação nas classes de dados compiladas para as associar à implementação da JPA.

Copiar os JARs

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

Copie os ficheiros JAR para o diretório war/WEB-INF/lib/ da sua aplicação.

Certifique-se de que o appengine-api.jar também está no diretório war/WEB-INF/lib/. (Pode já ter copiado este código quando criou o projeto.) O plug-in DataNucleus do App Engine usa este JAR para aceder ao datastore.

Criar o ficheiro persistence.xml

A interface JPA precisa de um ficheiro de configuração denominado persistence.xml no diretório war/WEB-INF/classes/META-INF/ da aplicação. Pode criar este ficheiro nesta localização diretamente ou fazer com que o processo de compilação copie este ficheiro de um diretório de origem.

Crie o ficheiro 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 do armazenamento de dados e prazo da chamada

Conforme descrito na página Consultas Datastore, pode definir a política de leitura (consistência forte vs. consistência eventual) e o prazo da chamada datastore para um EntityManagerFactory no ficheiro persistence.xml. Estas definições são colocadas no elemento <persistence-unit>. Todas as chamadas feitas com uma determinada instância EntityManager usam a configuração selecionada quando o gestor foi criado pelo EntityManagerFactory. Também pode substituir estas opções para um indivíduo Query (descrito abaixo).

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

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

Pode definir prazos separados de chamadas da base de dados para leituras e escritas. Para leituras, use a propriedade padrão JPA javax.persistence.query.timeout. Para escritas, use datanucleus.datastoreWriteTimeout. O valor é um período, em milissegundos.

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

Se quiser usar transações entre grupos (XG), adicione a seguinte propriedade:

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

Pode ter vários elementos <persistence-unit> no mesmo ficheiro persistence.xml, usando diferentes atributos name, para usar instâncias EntityManager com diferentes configurações na mesma app. Por exemplo, o seguinte ficheiro persistence.xml estabelece dois conjuntos de configuração, 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>

Consulte a secção Obter uma instância do EntityManager abaixo para ver informações sobre a criação de um EntityManager com um conjunto de configurações nomeado.

Pode substituir a política de leitura e o prazo de chamada para um objeto Query individual. Para substituir a política de leitura de um Query, chame o respetivo método setHint() da seguinte forma:

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

Tal como acima, os valores possíveis são "EVENTUAL" e "STRONG".

Para substituir o limite de tempo de leitura, chame setHint() da seguinte forma:

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

Não existe forma de substituir a configuração destas opções quando obtém entidades por chave.

Melhorar as classes de dados

A implementação do DataNucleus da JPA usa um passo de "melhoramento" pós-compilação no processo de compilação para associar classes de dados à implementação da JPA.

Pode executar o passo de melhoramento em classes compiladas a partir da linha de comandos com o seguinte comando:

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

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

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

Obter uma instância de EntityManager

Uma app interage com a JPA através de uma instância da classe EntityManager. Obtém esta instância ao instanciar e chamar um método numa instância da classe EntityManagerFactory. A fábrica usa a configuração JPA (identificada pelo nome "transactions-optional") para criar instâncias EntityManager.

Uma vez que uma instância EntityManagerFactory demora a ser inicializada, é recomendável reutilizar uma única instância o máximo possível. Uma forma fácil de o fazer é criar uma classe wrapper singleton com uma instância estática, da seguinte 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 do conjunto de configurações no ficheiro persistence.xml. Se a sua app usar vários conjuntos de configuração, tem de estender este código para chamar Persistence.createEntityManagerFactory() conforme pretendido. O seu código deve armazenar em cache uma instância singleton de cada EntityManagerFactory.

A app usa a instância de fábrica para criar uma instância EntityManager para cada pedido que acede ao armazenamento de dados.

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

import EMF;

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

Usa a EntityManager para armazenar, atualizar e eliminar objetos de dados, bem como para executar consultas da base de dados.

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

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

Anotações de classe e campo

Cada objeto guardado pelo JPA torna-se uma entidade no arquivo 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, com o nome da propriedade igual ao nome do campo (com a capitalização preservada).

Para declarar uma classe Java como capaz de ser armazenada e obtida a partir do armazenamento de dados com JPA, atribua à classe uma anotação @Entity. Por exemplo:

import javax.persistence.Entity;

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

Os campos da classe de dados que vão ser armazenados no arquivo de dados têm de ser de um tipo que seja persistente por predefinição ou declarado explicitamente como persistente. Pode encontrar um gráfico que detalha o comportamento de persistência predefinido da JPA no Website da DataNucleus. Para declarar explicitamente um campo como persistente, atribui-lhe uma 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 dos seguintes:

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

Uma classe de dados tem de ter um construtor predefinido público ou protegido e um campo dedicado ao armazenamento da chave principal da entidade de armazenamento de dados correspondente. Pode escolher entre quatro tipos diferentes de campos de chave, cada um com um tipo de valor e anotações diferentes. (Consulte o artigo Criar dados: chaves para mais informações.) O campo de chave mais simples é um valor inteiro longo que é preenchido automaticamente pela JPA com um valor exclusivo em todas as outras instâncias da classe quando o objeto é guardado no armazenamento de dados pela primeira vez. As chaves de números inteiros longos usam 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;

Segue-se um exemplo de uma 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

O JPA suporta a criação de classes de dados que usam a herança. Antes de falarmos sobre como funciona a herança JPA no App Engine, recomendamos que leia a documentação do DataNucleus sobre este assunto e, em seguida, volte. Concluído? OK. A herança de JPA no App Engine funciona conforme descrito na documentação do DataNucleus, com algumas restrições adicionais. Vamos abordar estas restrições e, em seguida, apresentar alguns exemplos concretos.

A estratégia de herança "JOINED" permite dividir os dados de um único objeto de dados em várias "tabelas", mas, uma vez que o datastore do App Engine não suporta junções, a operação num objeto de dados com esta estratégia de herança requer uma chamada de procedimento remoto para cada nível de herança. Isto é potencialmente muito ineficiente, pelo que a estratégia de herança "JOINED" não é suportada em classes de dados.

Em segundo lugar, a estratégia de herança "SINGLE_TABLE" permite-lhe armazenar os dados de um objeto de dados numa única "tabela" associada à classe persistente na raiz da sua hierarquia de herança. Embora não existam ineficiências inerentes nesta estratégia, esta não é suportada atualmente. Podemos rever esta situação em versões futuras.

Agora, as boas notícias: 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;
}

Neste exemplo, adicionámos uma anotação @MappedSuperclass à declaração da classe Worker. Isto indica ao JPA para armazenar todos os campos persistentes de Worker nas entidades de armazenamento de dados das respetivas subclasses. A entidade da base de dados criada como resultado da chamada persist() com uma instância Employee terá duas propriedades denominadas "department" e "salary". A entidade da base de dados criada como resultado da chamada persist() com uma instância Intern tem duas propriedades denominadas "department" e "internshipEndDate". Não vão existir entidades do tipo "Worker" no arquivo de dados.

Agora, vamos tornar as coisas um pouco mais interessantes. Suponhamos que, além de ter 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;
}

Neste exemplo, adicionámos uma anotação @Inheritance à declaração da classe FormerEmployee com o respetivo atributo strategy definido como InheritanceType.TABLE_PER_CLASS. Isto indica ao JPA que deve armazenar todos os campos persistentes de FormerEmployee e das respetivas superclasses em entidades de armazenamento de dados correspondentes a instâncias de FormerEmployee. A entidade da base de dados criada como resultado da chamada persist() com uma instância FormerEmployee terá três propriedades denominadas "department", "salary" e "lastDay". Nunca vai existir uma entidade do tipo "Employee" que corresponda a um FormerEmployee, mas se chamar persist() com um objeto cujo tipo de tempo de execução seja Employee, vai criar uma entidade do tipo "Employee".

A combinação de relações com a herança funciona desde que os tipos declarados dos campos de relação correspondam aos tipos de tempo de execução dos objetos que está a atribuir a esses campos. Consulte a secção sobre Relações Polimórficas para mais informações. Esta secção contém exemplos de JDO, mas os conceitos e as restrições são os mesmos para JPA.

Funcionalidades não suportadas do JPA 2.0

As seguintes funcionalidades da interface JPA não são suportadas pela implementação do App Engine:

  • Relações de muitos-para-muitos pertencentes.
  • Consultas "join". Não pode usar um campo de uma entidade secundária num filtro quando executar uma consulta no tipo principal. Tenha em atenção que pode testar o campo de relação do elemento principal diretamente numa consulta através de uma chave.
  • Consultas de agregação (group by, having, sum, avg, max, min)
  • Consultas polimórficas. Não pode executar uma consulta de uma classe para obter instâncias de uma subclasse. Cada classe é representada por um tipo de entidade separado no arquivo de dados.