Como usar 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 usando a API Datastore de nível inferior ou uma das APIs de código aberto desenvolvidas especificamente para o Datastore, como 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.

A versão 1.x do plug-in está incluída no SDK para Java do App Engine, que implementa a JPA versão 1.0. A implementação é baseada em DataNucleus Access Platform versão 1.1.

Observação: as instruções nesta página se aplicam à JPA versão 1, que usa a versão 1.x do plug-in DataNucleus para o App Engine. A versão 2.x do plug-in DataNucleus, que permite usar a JPA 2.0, também está disponível. O plug-in 2.x fornece uma série de novos recursos e APIs. No entanto, a atualização não é totalmente compatível com a versão 1.x. Se você recriar um aplicativo usando o JPA 2.0, precisará atualizar e testar seu código. Para mais informações sobre a nova versão, consulte Como usar JPA 2.0 com o App Engine.

Como configurar a JPA

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

  • A JPA e os JARs de armazenamento de dados precisam estar no diretório war/WEB-INF/lib/ do aplicativo.
  • Um arquivo de configuração chamado persistence.xml precisa estar no diretório war/WEB-INF/classes/META-INF/ do aplicativo, com configuração que instrua a JPA a usar o App Engine Datastore.
  • o processo de criação do projeto execute uma etapa de "aprimoramento" pós-compilação nas classes de dados compiladas para associá-las à implementação da JPA.

Se você usar o Apache Ant para criar seu projeto, poderá utilizar uma tarefa Ant incluída no SDK para realizar a etapa de aprimoramento. É necessário copiar os JARs e criar o arquivo de configuração para seu projeto.

Como copiar os JARs

JPA e os JARs do armazenamento de dados estão incluídos no SDK para Java do Google App Engine. Você os encontra no diretório appengine-java-sdk/lib/user/orm/.

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

Certifique-se de que appengine-api.jar também esteja 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 chamado 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.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </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, você pode definir a política de leitura (consistência forte versus consistência eventual) e a duração máxima da chamada ao armazenamento de dados para um EntityManagerFactory no arquivo persistence.xml. Essas configurações vão para o 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. É possível também substituir essas opções por um Query individual (descrito abaixo).

Para definir a política de leitura, inclua uma propriedade chamada 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 da chamada 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" />

É possível pode ter vários elementos <persistence-unit> no mesmo arquivo persistence.xml, usando atributos name diferentes, para usar instâncias de EntityManager com configurações diferentes no mesmo aplicativo. Por exemplo, o seguinte arquivo 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.store.appengine.jpa.DatastorePersistenceProvider</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.store.appengine.jpa.DatastorePersistenceProvider</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" />
        </properties>
    </persistence-unit>
</persistence>

Consulte Como conseguir uma instância de EntityManager abaixo para ter informações sobre como criar um EntityManager com um conjunto de configurações nomeado.

A política de leitura e o prazo da chamada podem ser substituídos por um objeto Query individual. Para substituir a política de leitura por um Query, chame seu método setHint() da seguinte maneira:

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

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

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

        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.

Se estiver usando o Apache Ant, o SDK incluirá uma tarefa Ant para realizar essa etapa.

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 caminho de classe precisa conter as 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 classes de dados.

Para mais informações sobre o otimizador de bytecode do DataNucleus, consulte a documentação do DataNucleus (em inglês).

Como receber uma instância de EntityManager

Um aplicativo interage com a JPA usando uma instância da classe EntityManager. Você consegue essa instância instanciando e chamando um método em uma instância da classe EntityManagerFactory. A fábrica usa a configuração JPA (identificada pelo nome "transactions-optional") para criar instâncias EntityManager.

Como uma instância EntityManagerFactory leva tempo para ser inicializada, é recomendável reutilizar uma única instância o máximo 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" se refere ao nome do conjunto de configuração no arquivo persistence.xml. Caso seu aplicativo use vários conjuntos de configuração, você precisará estender esse código para chamar Persistence.createEntityManagerFactory(), conforme quiser. Seu código precisa armazenar em cache uma instância singleton de cada EntityManagerFactory.

O aplicativo usa a instância de fábrica para criar uma instância 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 para executar consultas do armazenamento de dados.

Quando terminar de usar a instância EntityManager, chame o método close(). É um erro usar a instância 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 uma classe Java como capaz de ser armazenada e recuperada do armazenamento de dados com JPA, atribua uma anotação @Entity à classe. 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, você deve atribuir-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 destes:

  • um dos tipos principais compatíveis com o armazenamento de dados
  • um 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. As chaves de valor inteiro longo 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;

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 (link em inglês) sobre esse assunto e retorne depois. Terminou? OK. 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;
}

Neste exemplo, adicionamos uma anotação @MappedSuperclass à declaração de classe Worker. Isso diz à JPA que armazene todos os campos persistentes de Worker nas entidades de armazenamento de dados de suas subclasses. A entidade de armazenamento de dados criada como resultado da chamada de persist() com uma instância Employee terá duas propriedades chamadas "department" e "salary". A entidade de armazenamento de dados criada como resultado da chamada de persist() com uma instância Intern terá duas propriedades chamadas “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 ter Employee e Intern, também queiramos uma especialização de Employee que descreva os funcionários que deixaram a 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, adicionamos uma anotação @Inheritance à declaração de classe FormerEmployee com seu atributo strategy definido como InheritanceType.TABLE_PER_CLASS. Isso faz com que a JPA armazene todos os campos persistentes de FormerEmployee e suas superclasses nas entidades de armazenamento de dados correspondentes às instâncias FormerEmployee. A entidade de armazenamento de dados criada como resultado da chamada persist() com uma instância FormerEmployee terá três propriedades chamadas "department", "salary" e "lastDay". Nunca haverá uma entidade do tipo "Employee" que corresponda a uma FormerEmployee, mas se você chamar persist() com um objeto que tenha o tipo de ambiente de execução 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 com a JPA 1.0

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

  • Relacionamentos proprietários muitos para muitos e relacionamentos não proprietários. Você pode implementar relacionamentos não proprietários usando valores de chave explícitos, mas a verificação de tipo não é aplicada na API.
  • Consultas de junção. 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.