Usa JPA con App Engine

La API de Java Persistence (JPA) en una interfaz estándar para acceder a bases de datos en Java que proporciona una asignación automática entre clases de Java y tablas de bases de datos. Hay un complemento de código abierto disponible para usar JPA con Datastore y, en esta página, se proporciona información sobre cómo comenzar a usarlo.

Advertencia: Creemos que la mayoría de los desarrolladores tendrá una mejor experiencia con la API de Datastore de bajo nivel, o bien con una de las API de código abierto diseñadas específicamente para Datastore, como Objectify. JPA se diseñó para usarse con bases de datos relacionales tradicionales y, por lo tanto, no puede representar de manera explícita algunos de los aspectos de Datastore que lo diferencian de las bases de datos relacionales, como los grupos de entidad y las consultas principales. Este hecho puede ocasionar problemas sutiles que son difíciles de entender y corregir.

Se incluye la Versión 1.x del complemento en el SDK de Java de App Engine, el cual implementa la versión 1.0 de JPA. La implementación se basa en la versión 1.1 de DataNucleus Access Platform.

Nota: Las instrucciones incluidas en esta página se aplican a la versión 1 de JPA, que utiliza la versión 1.x del complemento DataNucleus para App Engine. La versión 2.x del complemento DataNucleus también está disponible, lo que te permite usar JPA 2.0. El complemento 2.x proporciona una serie de API y características nuevas. Sin embargo, la actualización no es totalmente compatible con la versión 1.x. Si vuelves a compilar una aplicación con JPA 2.0, debes actualizar y restablecer tu código. Para obtener más información sobre la nueva versión, consulta Usa JPA 2.0 con App Engine.

Configura JPA 2.0

Una app de App Engine necesita lo siguiente para acceder al almacén de datos con JPA:

  • Los JPA y JAR del almacén de datos deben estar en el directorio war/WEB-INF/lib/ de la app.
  • Un archivo de configuración llamado persistence.xml debe estar en el directorio war/WEB-INF/classes/META-INF/ de la app, con una configuración que le indique a JPA que use el almacén de datos de App Engine.
  • El proceso de compilación del proyecto debe realizar un paso de "mejoras" posterior a la compilación en las clases de datos compiladas para asociarlas con la implementación de JPA.

Copia archivos JAR

Los archivos JAR de JPA y del almacén de datos se incluyen en el SDK de App Engine para Java. Puedes encontrarlos en el directorio appengine-java-sdk/lib/user/orm/.

Copia los archivos JAR en el directorio war/WEB-INF/lib/ de la aplicación.

Asegúrate de que appengine-api.jar también esté en el directorio war/WEB-INF/lib/. (es posible que ya lo hayas copiado cuando creaste el proyecto). El complemento de DataNucleus en App Engine usa este archivo JAR para acceder al almacén de datos.

Crea el archivo persistence.xml

La interfaz de JPA necesita un archivo de configuración llamado persistence.xml en el directorio war/WEB-INF/classes/META-INF/ de la aplicación. Puedes crear este archivo directo en esta ubicación o solicitar al proceso de compilación que copie este archivo desde un directorio fuente.

Crea el archivo con el siguiente contenido:

<?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 lectura y plazo de llamada de Datastore

Como se describe en la página Consultas de Datastore, puedes configurar la política de lectura (coherencia sólida frente a coherencia eventual) y el plazo de llamada al almacén de datos para una EntityManagerFactory en el archivo persistence.xml. Estos parámetros de configuración se incluyen en el elemento <persistence-unit>. Todas las llamadas realizadas con una instancia EntityManager determinada usan la configuración seleccionada cuando EntityManagerFactory creó al administrador. También puedes anular estas opciones para una Query individual (como se describe a continuación).

Para establecer la política de lectura, incluye una propiedad llamada datanucleus.appengine.datastoreReadConsistency. Sus valores posibles son EVENTUAL (para lecturas con coherencia eventual) y STRONG (para lecturas con coherencia sólida). Si no se especifica, el valor predeterminado es STRONG.

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

Puedes establecer plazos de llamada al almacén de datos de forma individual para lecturas y escrituras. Para las lecturas, usa la propiedad estándar de JPA javax.persistence.query.timeout. Para escrituras, usa datanucleus.datastoreWriteTimeout. El valor es una cantidad de tiempo en milisegundos.

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

Si deseas usar transacciones entre grupos (XG), agrega la siguiente propiedad:

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

Puedes tener varios elementos <persistence-unit> en el mismo archivo persistence.xml, con atributos name diferentes, para usar instancias EntityManager con configuraciones diferentes en la misma app. Por ejemplo, el siguiente archivo persistence.xml establece dos conjuntos de configuración, uno llamado "transactions-optional" y otro llamado "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>

Consulta Obtén una instancia de EntityManager a continuación para obtener información sobre cómo crear un EntityManager con un conjunto de configuraciones con nombre.

Puedes anular la política de lectura y el plazo de llamada para un objeto Query individual. A fin de anular la política de lectura de un Query, llama a su método setHint() de la siguiente manera:

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

Como se indicó antes, los valores posibles son "EVENTUAL" y "STRONG".

Para anular el tiempo de espera de lectura, llama a setHint() de la siguiente manera:

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

No hay manera de anular la configuración para estas opciones cuando recuperas entidades por clave.

Mejora las clases de datos

La implementación de JPA de DataNucleus usa un paso de “mejoras” posterior a la compilación para asociar las clases de datos con la implementación de JPA.

Desde la línea de comandos, puedes realizar el paso de mejora sobre clases compiladas con este comando:

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

La ruta de clase debe contener los archivos JAR datanucleus-core-*.jar, datanucleus-jpa-*, datanucleus-enhancer-*.jar, asm-*.jar y geronimo-jpa-*.jar (en el que * es el número de versión apropiado de cada JAR) desde el directorio appengine-java-sdk/lib/tools/, así como todas tus clases de datos.

Para obtener más información sobre el enhancer del código de bytes de DataNucleus, consulta la documentación de DataNucleus.

Obtener una instancia de EntityManager

Una app interactúa con JPA mediante una instancia de la clase EntityManager. Para obtener esta instancia, se debe crear una instancia y llamar a un método en una instancia de la clase EntityManagerFactory. La fábrica usa la configuración de JPA (identificada por el nombre "transactions-optional") para crear instancias EntityManager.

Debido a que una instancia EntityManagerFactory tarda en inicializarse, es una buena idea reutilizar una instancia individual lo máximo posible. Una manera fácil de hacerlo es crear una clase wrapper de singleton con una instancia estática de la siguiente manera:

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;
    }
}

Sugerencia: "transactions-optional" hace referencia al nombre de la configuración establecida en el archivo persistence.xml. Si tu app usa varios conjuntos de configuración, tendrás que extender este código para llamar a Persistence.createEntityManagerFactory() si lo deseas. Tu código debería almacenar en caché una instancia singleton de cada EntityManagerFactory.

La app usa la instancia de fábrica a fin de crear una instancia EntityManager para cada solicitud que accede al almacén de datos.

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

import EMF;

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

Usa EntityManager para almacenar, actualizar y borrar objetos de datos, además de realizar consultas en el almacén de datos.

Cuando termines de usar la instancia EntityManager, debes llamar a su método close(). Usar la instancia EntityManager luego de llamar a su método close() es un error.

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

Anotaciones de clase y campo

Cada objeto que guarda JPA se convierte en una entidad en el almacén de datos de App Engine. El tipo de entidad deriva del nombre simple de la clase (sin el nombre del paquete). Cada campo persistente de la clase representa una propiedad de la entidad. El nombre de la propiedad es el mismo que el del campo (con el caso preservado).

Para declarar que una clase Java es capaz de almacenarse y recuperarse desde el almacén de datos con JPA, aplícale a la clase una anotación @Entity. Por ejemplo:

import javax.persistence.Entity;

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

Los campos de la clase de datos que deben guardarse en el almacén de datos deben ser persistentes de manera predeterminada o declararse así explícitamente. Puedes encontrar un gráfico que detalla el comportamiento de persistencia predeterminado de JPA en el sitio web de DataNucleus. Para declarar un campo como persistente de forma explícita, le debes aplicar una anotación @Basic:

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

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

// ...
    @Basic
    private ShortBlob data;

El tipo de un campo puede ser cualquiera de los siguientes:

  • Uno de los tipos principales compatibles con el almacén de datos
  • Una colección (como java.util.List<...>) de valores de un tipo de almacén de datos principal
  • Una instancia o una colección de instancias de clase @Entity
  • Una clase integrada almacenada como propiedades en la entidad

Una clase de datos debe tener un constructor predeterminado público o protegido y un campo dedicado a almacenar la clave principal de la entidad correspondiente del almacén de datos. Puedes elegir entre cuatro tipos diferentes de campos de clave, cada uno con un tipo de valor y anotaciones diferentes (consulta Crea datos: Claves para obtener más información). El campo de clave más simple es un valor de número entero largo que JPA propaga de manera automática con un valor único en todas las demás instancias de la clase cuando el objeto se guarda en el almacén de datos por primera vez. Las claves de números enteros largos usan una anotación @Id y una anotación @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;

Aquí tienes un ejemplo de clases de datos:

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;
    }
}

Herencia

JPA admite la creación de clases de datos que usan herencia. Antes de presentar información sobre el funcionamiento de la herencia JPA en App Engine, te recomendamos leer la documentación de DataNucleus sobre este tema y luego regresar. ¿Listo? De acuerdo. La herencia JPA en App Engine funciona como se describe en la documentación de DataNucleus con algunas restricciones adicionales. Analizaremos estas restricciones y daremos algunos ejemplos concretos.

La estrategia de herencia "JOINED" te permite dividir los datos de un objeto de datos único en varias "tablas", pero debido a que el almacén de datos de App Engine no admite uniones, operar en un objeto de datos con esta estrategia de herencia requiere una llamada de procedimiento remoto para cada nivel de herencia. Esto es potencialmente muy ineficiente, por lo que la estrategia de herencia "JOINED" no es compatible con las clases de datos.

En segundo lugar, la estrategia de herencia "SINGLE_TABLE" te permite almacenar los datos de un objeto de datos en una sola "tabla" asociada con la clase persistente en la raíz de tu jerarquía de herencia. Aunque no hay ineficiencias heredadas en esta estrategia, por el momento no se admite. Podríamos reconsiderarlo en futuras versiones.

Ahora las buenas noticias: Las estrategias "TABLE_PER_CLASS" y "MAPPED_SUPERCLASS" funcionan como se describe en la documentación de DataNucleus. Veamos un ejemplo:

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;
}

En este ejemplo, agregamos una anotación @MappedSuperclass a la declaración de clase Worker. Esto le indica a JPA que almacene todos los campos persistentes del Worker en las entidades del almacén de datos de sus subclases. La entidad del almacén de datos creada como resultado de una llamada a persist() con una instancia Employee tendrá dos propiedades denominadas “department” y “salary”. La entidad del almacén de datos creada como resultado de una llamada a persist() con una instancia Intern tendrá dos propiedades llamadas “department” y “inernshipEndDate”. No habrá ninguna entidad del tipo "Worker" en el almacén de datos.

Ahora hagamos esto un poco más interesante. Supongamos que, además de tener Employee y Intern, también queremos una especialización de Employee que describa a los empleados que abandonaron la 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;
}

En este ejemplo, agregamos una anotación @Inheritance a la declaración de clase FormerEmployee con su atributo strategy establecido en InheritanceType.TABLE_PER_CLASS. Esto le indica a JPA que almacene todos los campos persistentes de FormerEmployee y sus superclases en entidades de almacén de datos que correspondan a instancias FormerEmployee. La entidad del almacén de datos creada como resultado de una llamada de clase persist() con una instancia FormerEmployee tendrá tres propiedades denominadas “department”, “salary” y “lastDay”. Nunca existirá una entidad de tipo “Employee” que corresponda a FormerEmployee, pero si llamas a persist() con un objeto cuyo tipo de entorno de ejecución es Employee, crearás una entidad de categoría “Employee”.

La combinación de relaciones con herencia funciona siempre que los tipos declarados de tus campos de relación coincidan con los tipos de entorno de ejecución de los objetos que asignas a esos campos. Consulta la sección en Relaciones polimórficas para obtener más información. Esta sección contiene ejemplos de JDO, pero los conceptos y las restricciones son los mismos para JPA.

Características no compatibles de JPA 1.0

Las siguientes características de la interfaz de JPA no son compatibles con la implementación en App Engine:

  • Relaciones de varios a varios con propietario y relaciones sin propietario. Puedes implementar relaciones sin propietario mediante valores de clave explícitos, aunque la comprobación de tipo no se aplica en la API.
  • Consultas del tipo "Join". No puedes usar un campo de una entidad secundaria en un filtro cuando realizas una consulta sobre el tipo principal. Ten en cuenta que puedes probar el campo de relación de la entidad principal de forma directa mediante una consulta con clave.
  • Consultas de agregación (group by, having, sum, avg, max, min).
  • Consultas polimórficas. No puedes realizar una consulta de una clase para obtener instancias de una subclase. Cada clase se representa con un tipo de entidad individual en el almacén de datos.