Java Persistence API (JPA) es una interfaz estándar para acceder a bases de datos en Java que proporciona una asignación automática entre las clases de Java y las tablas de bases de datos. Hay un complemento de código abierto disponible para usar JPA con Datastore. En esta página se explica cómo empezar a usarlo.
Advertencia: Creemos que la mayoría de los desarrolladores disfrutarán de una mejor experiencia si usan la API Datastore de bajo nivel o una de las APIs de código abierto desarrolladas específicamente para Datastore, como Objectify. JPA se diseñó para usarse con bases de datos relacionales tradicionales, por lo que no tiene forma de representar explícitamente algunos de los aspectos de Datastore que lo diferencian de las bases de datos relacionales, como los grupos de entidades y las consultas de ancestros. Esto puede provocar problemas sutiles que son difíciles de entender y solucionar.
La versión 1.x del complemento se incluye en el SDK de Java de App Engine, que 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 de esta página se aplican a la versión 1 de JPA, que usa la versión 1.x del complemento DataNucleus para App Engine. También está disponible la versión 2.x del complemento DataNucleus, que te permite usar JPA 2.0. El complemento 2.x proporciona varias APIs y funciones nuevas, pero la actualización no es totalmente retrocompatible con la versión 1.x. Si recompilas una aplicación con JPA 2.0, debes actualizar y volver a probar tu código. Para obtener más información sobre la nueva versión, consulta el artículo sobre cómo usar JPA 2.0 con App Engine.
Configuración de JPA
Para usar JPA y 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 aplicación. - Debe haber un archivo de configuración llamado
persistence.xml
en el directoriowar/WEB-INF/classes/META-INF/
de la aplicación, 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 "mejora" posterior a la compilación en las clases de datos compiladas para asociarlas a la implementación de JPA.
Copia de los archivos JAR
Los archivos JAR de JPA y del almacén de datos se incluyen en el SDK de Java de 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 tu aplicación.
Asegúrate de que appengine-api.jar
también esté en el directorio war/WEB-INF/lib/
. (Puede que ya lo hayas copiado al crear el proyecto). El complemento DataNucleus de App Engine usa este archivo JAR para acceder al almacén de datos.
Creación del archivo persistence.xml
La interfaz 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 directamente en esta ubicación o hacer que tu proceso de compilación lo copie desde un directorio de origen.
Debes crear 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 del almacén de datos y fecha límite de llamada
Como se describe en la página Consultas de Datastore, puedes definir la política de lectura (coherencia fuerte o coherencia final) y el plazo de la llamada a Datastore para un EntityManagerFactory
en el archivo persistence.xml
.
Estos ajustes se colocan en el elemento <persistence-unit>
. Todas las llamadas realizadas con una instancia de EntityManager
determinada usan la configuración seleccionada cuando el gestor fue creado por EntityManagerFactory
. También puedes anular estas opciones para un Query
concreto (se describe más abajo).
Para definir la política de lectura, incluye una propiedad llamada datanucleus.appengine.datastoreReadConsistency
. Sus valores posibles son EVENTUAL
(para lecturas con coherencia retardada) y STRONG
(para lecturas con coherencia inmediata). Si no se especifica, el valor predeterminado es STRONG
.
<property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
Puedes definir distintos tiempos límite en las invocaciones del almacén de datos tanto para procesos de lectura como de escritura. Para las lecturas, usa la propiedad estándar de JPA javax.persistence.query.timeout
. Para las escrituras, usa
datanucleus.datastoreWriteTimeout
. El valor es un periodo de tiempo en milisegundos.
<property name="javax.persistence.query.timeout" value="5000" /> <property name="datanucleus.datastoreWriteTimeout" value="10000" />
Si quieres usar transacciones entre grupos (XG), añade la siguiente propiedad:
<property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />
Puedes tener varios elementos <persistence-unit>
en el mismo archivo persistence.xml
, con diferentes atributos name
, para usar instancias de EntityManager
con diferentes configuraciones en la misma aplicación. 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 Obtener una instancia de EntityManager más abajo para obtener información sobre cómo crear un EntityManager
con un conjunto de configuración con nombre.
Puedes anular la política de lectura y la fecha límite de llamada de un objeto Query
concreto. Para 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");
Al igual que en el caso anterior, 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 forma de anular la configuración de estas opciones cuando obtienes entidades por clave.
Mejora de las clases de datos
La implementación de JPA de DataNucleus usa un paso de "mejora" posterior a la compilación en el proceso de compilación para asociar clases de datos con la implementación de JPA.
Puede realizar el paso de mejora en las clases compiladas desde la línea de comandos con el siguiente comando:
java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer class-files
La ruta de clases debe contener los archivos JAR datanucleus-core-*.jar
, datanucleus-jpa-*
, datanucleus-enhancer-*.jar
, asm-*.jar
y geronimo-jpa-*.jar
(donde *
es el número de versión adecuado de cada archivo JAR) del directorio appengine-java-sdk/lib/tools/
, así como todas tus clases de datos.
Para obtener más información sobre el optimizador de bytecode de DataNucleus, consulta la documentación de DataNucleus.
Obtener una instancia de EntityManager
Una aplicación interactúa con JPA mediante una instancia de la clase EntityManager
. Para obtener esta instancia, debes crear una instancia de la clase EntityManagerFactory
y llamar a un método en ella. La fábrica usa la configuración de JPA (identificada por el nombre "transactions-optional"
) para crear instancias de EntityManager
.
Como una instancia de EntityManagerFactory
tarda en inicializarse, es recomendable reutilizar una sola instancia tanto como sea posible. Una forma sencilla de hacerlo es crear una clase envolvente singleton con una instancia estática, como se indica a continuación:
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; } }
Nota: "transactions-optional"
hace referencia al nombre del conjunto de configuración del archivo persistence.xml
. Si tu aplicación usa varios conjuntos de configuración, tendrás que ampliar este código para llamar a Persistence.createEntityManagerFactory()
como quieras. Tu código debe almacenar en caché una instancia singleton de cada EntityManagerFactory
.
La aplicación usa la instancia de fábrica para crear una instancia de EntityManager
por cada solicitud que acceda al almacén de datos.
import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import EMF; // ... EntityManager em = EMF.get().createEntityManager();
Utilizas EntityManager
para almacenar, actualizar y eliminar objetos de datos, así como para realizar consultas de almacén de datos.
Cuando hayas terminado con la instancia EntityManager
, debes llamar a su método close()
. Es un error usar la instancia EntityManager
después de llamar a su método close()
.
try { // ... do stuff with em ... } finally { em.close(); }
Anotaciones de clase y de campo
Cada objeto que guarda JPA se convierte en una entidad del almacén de datos de App Engine. El tipo de entidad se deriva del nombre simple de la clase (sin el nombre del paquete). Cada campo persistente de la clase representa una propiedad de la entidad, cuyo nombre es igual al nombre del campo (con las mayúsculas y minúsculas conservadas).
Para declarar que una clase de Java se puede almacenar y recuperar de Datastore con JPA, asigna a la clase la anotación @Entity
. Por ejemplo:
import javax.persistence.Entity; @Entity public class Employee { // ... }
Los campos de la clase de datos que se van a almacenar en el almacén de datos deben ser de un tipo que se conserve de forma predeterminada o declararse explícitamente como persistentes.
Puedes consultar un gráfico que detalla el comportamiento de persistencia predeterminado de JPA en el
sitio web de DataNucleus. Para declarar explícitamente un campo como persistente, debes
asignarle una anotación @Basic
:
import java.util.Date; import javax.persistence.Enumerated; import com.google.appengine.api.datastore.ShortBlob; // ... @Basic private ShortBlob data;
A continuación, se indican los tipos de campo existentes.
- uno de los tipos principales admitidos por el almacén de datos,
- una colección (como un
java.util.List<...>
) de valores de un tipo de almacén de datos principal - Una instancia o una colección de instancias de una clase
@Entity
- una clase insertada, 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 del almacén de datos correspondiente. Puedes elegir entre cuatro tipos de campos clave, cada uno con un tipo de valor y anotaciones diferentes. (Consulta Crear datos: claves para obtener más información). El campo de clave más sencillo es un valor entero largo que JPA rellena automáticamente 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;
A continuación, te ofrecemos un ejemplo de clase 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 utilicen herencia. Antes de hablar sobre cómo funciona la herencia de JPA en App Engine, te recomendamos que leas la documentación de DataNucleus sobre este tema y que vuelvas después. ¿Hecho? De acuerdo. La herencia de JPA en App Engine funciona como se describe en la documentación de DataNucleus, con algunas restricciones adicionales. Hablaremos de estas restricciones y, después, pondremos algunos ejemplos concretos.
La estrategia de herencia "JOINED" te permite dividir los datos de un solo objeto de datos en varias "tablas", pero como el almacén de datos de App Engine no admite combinaciones, para operar en un objeto de datos con esta estrategia de herencia, se requiere una llamada a procedimiento remoto para cada nivel de herencia. Esto puede ser muy ineficiente, por lo que la estrategia de herencia "JOINED" no se admite en 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 a la clase persistente en la raíz de tu jerarquía de herencia. Aunque esta estrategia no tiene ineficiencias inherentes, no se admite actualmente. Es posible que volvamos a tratar este tema en futuras versiones.
Ahora, las buenas noticias: las estrategias "TABLE_PER_CLASS" y "MAPPED_SUPERCLASS" funcionan tal 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, hemos añadido una anotación @MappedSuperclass
a la declaración de la clase Worker
. De esta forma, se indica a JPA que almacene todos los campos persistentes de Worker
en las entidades de Datastore de sus subclases. La entidad de almacén de datos creada como resultado de llamar a persist()
con una instancia de Employee
tendrá dos propiedades llamadas "department" y "salary". La entidad del almacén de datos creada como resultado de llamar a persist()
con una instancia de Intern
tendrá dos propiedades llamadas "department" y "internshipEndDate". No habrá ninguna entidad de tipo "Worker" en el almacén de datos.
Ahora vamos a hacer las cosas un poco más interesantes. Supongamos que, además de Employee
y Intern
, queremos una especialización de Employee
que describa a los empleados que han dejado 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, hemos añadido una anotación @Inheritance
a la declaración de clase FormerEmployee
con su atributo strategy
definido como InheritanceType.TABLE_PER_CLASS
. De esta forma, se indica a JPA que almacene todos los campos persistentes de FormerEmployee
y sus superclases en entidades de Datastore correspondientes a instancias de FormerEmployee
. La entidad de almacén de datos creada como resultado de llamar a persist()
con una instancia de FormerEmployee
tendrá tres propiedades llamadas "department", "salary" y "lastDay". Nunca habrá una entidad de tipo "Employee" que corresponda a un FormerEmployee
, pero si llamas a persist()
con un objeto cuyo tipo de tiempo de ejecución sea Employee
, crearás una entidad de tipo "Employee".
La combinación de relaciones con la herencia funciona siempre que los tipos declarados de los campos de relación coincidan con los tipos de tiempo de ejecución de los objetos que asignes a esos campos. Consulta la sección sobre 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.
Funciones no admitidas de JPA 1.0
La implementación de App Engine no admite las siguientes funciones de la interfaz JPA:
- Las relaciones de propiedad multidireccionales y las relaciones sin propiedad. Puedes implementar relaciones no propias mediante valores de clave explícitos, aunque la comprobación de tipos no se aplica en la API.
- Consultas "Join". No puedes usar un campo de una entidad secundaria en un filtro cuando realices una consulta en el tipo de entidad principal. Ten en cuenta que puedes probar el campo de relación de los padres directamente en una consulta mediante una clave.
- Consultas de agrupación conjunta (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 mediante un tipo de entidad independiente en el almacén de datos.