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.
El SDK de Java de App Engine incluye la versión 2.x del complemento DataNucleus para Datastore. Este complemento corresponde a la versión 3.0 de la plataforma de acceso de DataNucleus, que te permite usar el almacén de datos de App Engine a través de JPA 2.0.
Para obtener más información sobre JPA, consulta la documentación de Access Platform 3.0. En particular, consulta la documentación de JPA.
Advertencia: La versión 2.x del complemento DataNucleus para App Engine usa DataNucleus v3.x. El complemento 2.x no es totalmente compatible con el complemento 1.x anterior. Si actualizas a la nueva versión, asegúrate de actualizar y probar tu aplicación.
Herramientas de compilación compatibles con JPA 2.x y 3.0
Puedes usar Apache Ant o Maven para usar la versión 2.x o 3.0 del complemento DataNucleus para App Engine:
- Para los usuarios de Ant: el SDK incluye una tarea de Ant que realiza el paso de mejora. Debes copiar los archivos JAR y crear el archivo de configuración cuando configures tu proyecto.
- Para los usuarios de Maven: puedes mejorar las clases con las siguientes configuraciones en tu archivo
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 a la versión 2.x del complemento DataNucleus
En esta sección se proporcionan instrucciones para actualizar tu aplicación de forma que use la versión 2.x del complemento DataNucleus para App Engine, que corresponde a DataNucleus Access Platform 3.0 y JPA 2.0. La versión 2.x del complemento no es totalmente compatible con la versión 1.x y puede cambiar sin previo aviso. Si actualizas, asegúrate de actualizar y probar el código de tu aplicación.
Nuevos comportamientos predeterminados
La versión 2.x del complemento DataNucleus de App Engine tiene algunos valores predeterminados diferentes a la versión 1.x anterior:
- El proveedor de persistencia de JPA ahora es
org.datanucleus.api.jpa.PersistenceProviderImpl
. - El almacenamiento en caché de nivel 2 está habilitado de forma predeterminada. Para volver al comportamiento predeterminado anterior, asigna el valor none a la propiedad de persistencia
datanucleus.cache.level2.type
. También puedes incluir el complemento datanucleus-cache en la ruta de clases y definir la propiedad de persistenciadatanucleus.cache.level2.type
en javax.cache para usar Memcache en el almacenamiento en caché de nivel 2. - Ahora, Datastore
IdentifierFactory
usa datanucleus2 de forma predeterminada. Para obtener el comportamiento anterior, asigna el valor datanucleus1 a la propiedad persistencedatanucleus.identifierFactory
. - Las llamadas no transaccionales a
EntityManager.persist()
,EntityManager.merge()
yEntityManager.remove()
ahora se ejecutan de forma atómica. Antes, la ejecución se producía en la siguiente transacción o al alcanzarEntityManager.close()
. - JPA tiene
retainValues
habilitado, lo que significa que los valores de los campos cargados se conservan en los objetos después de una confirmación. javax.persistence.query.chunkSize
ya no se usa. Usadatanucleus.query.fetchSize
en su lugar.- Ya no hay ninguna excepción en la asignación de EMF duplicados. Si la propiedad de persistencia
datanucleus.singletonEMFForName
tiene el valor true, devolverá el EMF singleton asignado actualmente a ese nombre. - Ahora se admiten las relaciones sin propietario.
- Ahora se admite la identidad de Datastore.
Para ver la lista completa de nuevas funciones, consulta las notas de la versión.
Cambios en los archivos de configuración
Para actualizar tu aplicación y usar la versión 2.0 del complemento DataNucleus para App Engine, debes cambiar algunos ajustes de configuración en build.xml
y persistence.xml
. Si vas a configurar una aplicación nueva y quieres usar la versión más reciente del complemento DataNucleus, consulta la sección Configurar JPA 2.0.
Advertencia: Después de actualizar la configuración, debes probar el código de tu aplicación para asegurarte de que es compatible con versiones anteriores.
En build.xml
El objetivo copyjars
debe cambiar para adaptarse a
DataNucleus 2.x:
- El objetivo de
copyjars
ha cambiado. Actualiza esta sección:
<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>
- El objetivo de
datanucleusenhance
ha cambiado. Actualiza esta sección:
<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>
En persistence.xml
El objetivo de <provider>
ha cambiado. Actualiza esta sección:
<provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
to:
<provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
Configurar JPA 2.0
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/opt/user/datanucleus/v2/
.
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.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 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.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>
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.
Obtención de 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 con información detallada sobre 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? Vale. 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 2.0
La implementación de App Engine no admite las siguientes funciones de la interfaz JPA:
- Relaciones de propiedad multidireccionales.
- 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.