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 experiencia mejor con la API de Datastore de bajo nivel o una de las API de código abierto diseñadas especialmente 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 Usar JPA 2.0 con App Engine.

Configura JPA 2.0

Para usar JPA para acceder al almacén de datos, una aplicación de App Engine necesita lo siguiente:

  • Los archivos JAR de JPA y del almacén de datos deben estar en el directorio war/WEB-INF/lib/ de la app.
  • El 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 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.

Si usas Apache Ant para compilar tu proyecto, puedes usar una tarea de Ant que se incluye con el SDK para realizar el paso de mejoras. Debes copiar los archivos JAR y crear el archivo de configuración cuando configures el proyecto.

Cómo copiar archivos JAR

Los archivos JAR de JPA y del almacén de datos se incluyen con el SDK de Java en App Engine. 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 sobre consultas de Datastore, puedes configurar la política de lectura (coherencia sólida frente a eventual) y el plazo de llamada al almacén de datos para una EntityManagerFactory en el archivo persistence.xml. Esta configuración se incluye en el elemento <persistence-unit>. Todas las llamadas realizadas con una instancia EntityManager dada usan la configuración seleccionada cuando EntityManagerFactory creó el 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 configurar plazos de llamada al almacén de datos por separado para las lecturas y las 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 distintas en la misma app. Por ejemplo, en el siguiente archivo persistence.xml, se establecen dos conjuntos de configuraciones: uno se llama "transactions-optional" y el otro se llama "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 Obtener una instancia de EntityManager a continuación para recabar 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 para un objeto 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 explicó 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.

Si usas Apache Ant, el SDK incluye una tarea de Ant que realiza este paso.

Desde la línea de comandos, puedes realizar el paso de mejora en las clases compiladas con el siguiente 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 los que * es el número de versión adecuada 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 utilizando 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 con el nombre "transactions-optional") para crear instancias EntityManager.

Debido a que una instancia EntityManagerFactory tarda en inicializarse, conviene volver a usar una misma instancia 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 del conjunto de configuración en el archivo persistence.xml. Si tu app usa varios conjuntos de configuración, deberás 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 sobre 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 del 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 manera explícita, le debes aplicar una anotación @Basic como se muestra a continuación:

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) como se muestra a continuación:

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? Bien. 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 de clase persist() con una instancia Employee tendrá dos propiedades llamadas "department" y "salary". La entidad del almacén de datos creada como resultado de una llamada de clase 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 Intern y Employee, también queremos una especialización de Employee que describa a los empleados que han abandonado 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 configurado en InheritanceType.TABLE_PER_CLASS. Esto le dice a JPA que almacene todos los campos persistentes de FormerEmployee y sus superclases en las entidades del almacén de datos correspondientes a las 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 llamadas "department", "salary" y "lastDay". Nunca habrá una entidad del tipo "Employee" que corresponda a FormerEmployee, pero si llamas a persist() con un objeto cuyo 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.
¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Entorno estándar de App Engine para Java 8