Puedes usar JDO para almacenar objetos de datos de Java sin formato (a veces denominados "Plain Old Java Objects" u "objetos Java antiguos") en el almacén de datos. Cada objeto que se haga persistente con PersistenceManager se convertirá en una entidad del almacén de datos. Debes usar anotaciones para informar a JDO cómo almacenar y recrear instancias de tus clases de datos.
Nota: Las versiones anteriores de JDO usan archivos XML .jdo
en lugar de anotaciones de Java. Siguen funcionando con JDO 2.3. Esta documentación solo trata sobre el uso de anotaciones de Java con clases de datos.
Anotaciones de clase y de campo
Cada objeto que guarda JDO 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 (las clases internas usan la ruta $
sin el nombre del paquete). Cada campo persistente de la clase representa una propiedad de la entidad, con un nombre de propiedad idéntico al nombre del campo (con distinción de mayúsculas y minúsculas).
Para declarar que una clase de Java se puede almacenar y recuperar del almacén de datos con JDO, asigna a la clase una anotación @PersistenceCapable
. Por ejemplo:
import javax.jdo.annotations.PersistenceCapable; @PersistenceCapable public class Employee { // ... }
Los campos de la clase de datos que deben guardarse en el almacén de datos han de declararse como campos persistentes. Para declarar un campo como persistente, asígnale una anotación @Persistent
:
import java.util.Date; import javax.jdo.annotations.Persistent; // ... @Persistent private Date hireDate;
Para declarar que un campo no es persistente (no se almacena en el almacén de datos y no se restaura cuando se recupera el objeto), asígnale la anotación @NotPersistent
.
Nota: JDO especifica que los campos de determinados tipos son persistentes de forma predeterminada si no se especifican las anotaciones @Persistent
ni @NotPersistent
, y que los campos de todos los demás tipos no son persistentes de forma predeterminada. Consulta la documentación de DataNucleus para obtener una descripción completa de este comportamiento. Como no todos los tipos de valor principales del almacén de datos de App Engine son persistentes de forma predeterminada según la especificación de JDO, le recomendamos que anote explícitamente los campos como @Persistent
o @NotPersistent
para que quede claro.
A continuación, se indican los tipos de campo existentes. Encontrarás una descripción detallada de estos más abajo:
- uno de los tipos principales admitidos por el almacén de datos,
- una colección (como un
java.util.List<...>
) o una matriz de valores de un tipo de almacén de datos principal - Una instancia o una colección de instancias de una clase
@PersistenceCapable
- una instancia o una colección de instancias de una clase serializable,
- una clase insertada, almacenada como propiedades en la entidad.
Una clase de datos debe tener un solo campo dedicado al almacenamiento de la clave principal de la entidad correspondiente en el almacén de datos. Puedes elegir entre cuatro tipos de campo de clave distintos, cada uno de los cuales cuenta con un tipo de valor y anotaciones diferentes. Para obtener más información, consulta Crear datos: claves. El tipo de campo de clave más flexible es un objeto Key
, que JDO 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 principales de tipo Key
requieren una anotación @PrimaryKey
y una anotación @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
:
Nota: Define todos los campos persistentes como private
o protected
(o protegidos por el paquete) y solo proporciona acceso público a través de métodos de acceso. Es posible que el acceso directo a un campo persistente desde otra clase pueda evitar la mejora de clases de JDO, También puedes hacer que otras clases sean @PersistenceAware
. Consulta la documentación de DataNucleus para obtener más información.
import com.google.appengine.api.datastore.Key; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.PrimaryKey; // ... @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.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.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; @PersistenceCapable public class Employee { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private String firstName; @Persistent private String lastName; @Persistent private Date hireDate; public Employee(String firstName, String lastName, Date hireDate) { this.firstName = firstName; this.lastName = lastName; this.hireDate = hireDate; } // Accessors for the fields. JDO 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; } }
Tipos de valor principales
Para representar una propiedad que contenga un solo valor de un tipo principal, declara un campo del tipo Java y usa la anotación @Persistent
:
import java.util.Date; import javax.jdo.annotations.Persistent; // ... @Persistent private Date hireDate;
Objetos serializables
Un valor de campo puede contener una instancia de una clase serializable y almacenar el valor serializado de la instancia en un único valor de propiedad del tipo blob. Para indicar a JDO que serialice el valor, el campo usa la anotación @Persistent(serialized=true)
. Los valores blob no se indexan y no se pueden utilizar en criterios de ordenación ni en filtros de consultas.
A continuación, ofrecemos un ejemplo de una clase Serializable simple que representa un archivo que incluye el contenido y el nombre de este, así como un tipo MIME. No se trata de una clase de datos JDO, por lo que no hay anotaciones de persistencia.
import java.io.Serializable; public class DownloadableFile implements Serializable { private byte[] content; private String filename; private String mimeType; // ... accessors ... }
Para almacenar una instancia de una clase Serializable como valor Blob en una propiedad, declara un campo cuyo tipo sea la clase y usa la anotación @Persistent(serialized = "true")
:
import javax.jdo.annotations.Persistent; import DownloadableFile; // ... @Persistent(serialized = "true") private DownloadableFile file;
Objetos secundarios y relaciones
Un valor de campo que es una instancia de una clase @PersistenceCapable
crea una relación de propiedad individual entre dos objetos. Sin embargo, un campo que sea una colección de estas referencias creará una relación de propiedad de uno a varios objetos.
Importante: Las relaciones de propiedad tienen implicaciones en las transacciones, los grupos de entidades y las eliminaciones en cascada. Consulta más información en los artículos Transacciones y Relaciones.
A continuación, presentamos un ejemplo de relación de propiedad de uno a uno entre dos objetos, Employee y ContactInfo:
ContactInfo.java
import com.google.appengine.api.datastore.Key; // ... imports ... @PersistenceCapable public class ContactInfo { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private String streetAddress; @Persistent private String city; @Persistent private String stateOrProvince; @Persistent private String zipCode; // ... accessors ... }
Employee.java
import ContactInfo; // ... imports ... @PersistenceCapable public class Employee { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private ContactInfo myContactInfo; // ... accessors ... }
En este ejemplo, si la aplicación crea una instancia de Employee, rellena su campo myContactInfo
con una nueva instancia de ContactInfo y, a continuación, guarda la instancia de Employee con pm.makePersistent(...)
, el almacén de datos crea dos entidades. Uno es del tipo "ContactInfo"
, que representa la instancia ContactInfo. El otro es de tipo "Employee"
. La clave de la entidad ContactInfo
tiene la clave de la entidad Employee
como entidad superior del grupo de entidades.
Clases insertadas
Las clases insertadas te permiten configurar un valor de campo mediante una clase sin necesidad de crear una nueva entidad en el almacén de datos y de establecer una relación. Los campos del valor del objeto se guardan directamente en la entidad del almacén de datos cuando se trata de un objeto contenido.
Cualquier clase de datos de @PersistenceCapable
se puede usar como objeto insertado en otra clase de datos. Los campos @Persistent
de la clase se insertan en el objeto. Si le asignas a la clase la anotación @EmbeddedOnly
, solo se podrá usar como clase insertada. La clase insertada no requiere un campo de clave principal porque no se almacena como una entidad separada.
A continuación, presentamos un ejemplo de clase insertada. En este ejemplo se convierte la clase insertada en una clase interna de la clase de datos que la usa. Es algo útil, aunque no es necesario para que una clase pueda insertarse.
import javax.jdo.annotations.Embedded; import javax.jdo.annotations.EmbeddedOnly; // ... imports ... @PersistenceCapable public class EmployeeContacts { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) Key key; @PersistenceCapable @EmbeddedOnly public static class ContactInfo { @Persistent private String streetAddress; @Persistent private String city; @Persistent private String stateOrProvince; @Persistent private String zipCode; // ... accessors ... } @Persistent @Embedded private ContactInfo homeContactInfo; }
Los campos de una clase insertada se almacenan como propiedades en la entidad usando el nombre de cada campo y el nombre de la propiedad correspondiente. Si tienes más de un campo en el objeto cuyo tipo es una clase insertada, debes cambiar el nombre de los campos de uno de los campos. De este modo, no entran en conflicto entre sí. Para especificar los nombres de los campos, usa argumentos de la anotación @Embedded
. Por ejemplo:
@Persistent @Embedded private ContactInfo homeContactInfo; @Persistent @Embedded(members = { @Persistent(name="streetAddress", columns=@Column(name="workStreetAddress")), @Persistent(name="city", columns=@Column(name="workCity")), @Persistent(name="stateOrProvince", columns=@Column(name="workStateOrProvince")), @Persistent(name="zipCode", columns=@Column(name="workZipCode")), }) private ContactInfo workContactInfo;
Asimismo, los campos de los objetos no deben llamarse igual que los campos de las clases insertadas, salvo que se cambie el nombre de los campos insertados.
Como las propiedades persistentes de la clase insertada se almacenan en la misma entidad que los demás campos, puede usar campos persistentes de la clase insertada en filtros de consultas JDOQL y en criterios de ordenación. Puedes hacer referencia al campo insertado usando el nombre del campo externo, un punto (.
) y el nombre del campo insertado. Esto funciona tanto si se han cambiado los nombres de las propiedades de los campos insertados mediante anotaciones @Column
como si no.
select from EmployeeContacts where workContactInfo.zipCode == "98105"
Colecciones
Una propiedad del almacén de datos puede tener más de un valor. En JDO, esto se representa mediante un solo campo con un tipo Collection, donde la colección es de uno de los tipos de valor principales o una clase Serializable. Se admiten los siguientes tipos de colección:
java.util.ArrayList<...>
java.util.HashSet<...>
java.util.LinkedHashSet<...>
java.util.LinkedList<...>
java.util.List<...>
java.util.Map<...>
java.util.Set<...>
java.util.SortedSet<...>
java.util.Stack<...>
java.util.TreeSet<...>
java.util.Vector<...>
Si un campo se declara como List, los objetos que devuelve el almacén de datos tendrán un valor ArrayList. Si un campo se declara como Set, el almacén de datos devuelve un valor HashSet. Si un campo se declara como SortedSet, el almacén de datos devuelve un valor TreeSet.
Por ejemplo, un campo cuyo tipo es List<String>
se almacena como cero o más valores de cadena de la propiedad, uno por cada valor de List
.
import java.util.List; // ... imports ... // ... @Persistent List<String> favoriteFoods;
Una colección de objetos secundarios (de clases @PersistenceCapable
) crea varias entidades con una relación de uno a muchos. Consulta Relaciones.
Las propiedades del almacén de datos con más de un valor se comportan de forma especial para los filtros y los criterios de ordenación de consultas. Consulta la página Consultas de Datastore para obtener más información.
Campos de objeto y propiedades de entidad
El almacén de datos de App Engine distingue entre una entidad sin una propiedad determinada y una entidad con un valor null
para una propiedad. JDO no admite esta distinción: todos los campos de un objeto tienen un valor, posiblemente null
. Si se asigna el valor null
a un campo con un tipo de valor anulable (algo distinto de un tipo integrado, como int
o boolean
), cuando se guarde el objeto, la entidad resultante tendrá la propiedad definida con un valor nulo.
Si se carga una entidad de almacén de datos en un objeto y no tiene una propiedad para uno de los campos del objeto, y el tipo del campo es un tipo de valor único que admite valores nulos, el campo se define como null
. Cuando el objeto se vuelve a guardar en el almacén de datos, la propiedad null
se define en el almacén de datos como valor nulo. Si el tipo de campo no es de valor anulable, se generará una excepción al cargar una entidad sin la propiedad correspondiente. Esto no ocurrirá si la entidad se ha creado a partir de la misma clase JDO que se ha usado para recrear la instancia, pero puede ocurrir si la clase JDO cambia o si la entidad se ha creado con la API de nivel inferior en lugar de con JDO.
Si el tipo de un campo es una colección de un tipo de datos principal o una clase serializable y no hay valores para la propiedad en la entidad, la colección vacía se representa en el almacén de datos asignando a la propiedad un único valor nulo. Si el tipo del campo es un tipo de array, se le asigna un array de 0 elementos. Si el objeto se carga y la propiedad no tiene ningún valor, se le asigna al campo una colección vacía del tipo que corresponda. El almacén de datos detecta la diferencia entre una colección vacía y una colección con un único valor nulo.
Si la entidad tiene una propiedad sin un campo correspondiente en el objeto, no se podrá acceder a dicha propiedad desde el objeto. Si el objeto se vuelve a guardar en el almacén de datos, la propiedad adicional se elimina.
Si una entidad tiene una propiedad cuyo valor es de un tipo distinto al del campo correspondiente en el objeto, JDO intenta asignar dicho valor al tipo de campo. Si el valor no pudiera asignarse, JDO generaría una excepción ClassCastException. En los casos en que haya números (enteros largos y flotantes de ancho doble), no se asigna el valor, sino que se convierte. Si el valor de la propiedad numérica es mayor que el del tipo de campo, la conversión se lleva a cabo sin que se generen excepciones.
Para declarar que una propiedad no está indexada, añade la línea
@Extension(vendorName="datanucleus", key="gae.unindexed", value="true")
encima de la propiedad en la definición de la clase. Consulta la sección Propiedades sin indexar de la documentación principal para obtener más información sobre lo que significa que una propiedad no esté indexada.
Herencia
La creación de clases de datos que utilizan la herencia es algo natural y que puede hacerse con JDO. Antes de hablar sobre cómo funciona la herencia de JDO en App Engine, te recomendamos que leas la documentación de DataNucleus sobre este tema y vuelvas. ¿Hecho? Vale. La herencia de JDO en App Engine funciona tal como se describe en la documentación de DataNucleus, pero con algunas restricciones adicionales. Hablaremos de estas restricciones y, después, pondremos algunos ejemplos concretos.
La estrategia de herencia "new-table" te permite dividir los datos de un 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 por cada nivel de herencia. Esto puede ser muy ineficiente, por lo que la estrategia de herencia "new-table" no se admite en las clases de datos que no están en la raíz de sus jerarquías de herencia.
En segundo lugar, la estrategia de herencia de "tabla de superclase" le permite almacenar los datos de un objeto de datos en la "tabla" de su superclase. Aunque se trate de una técnica eficaz en sí misma, no es compatible con App Engine. No obstante, es posible que sea compatible en próximas versiones.
Ahora, las buenas noticias: las estrategias "subclass-table" y "complete-table" funcionan como se describe en la documentación de DataNucleus. Además, puedes usar "new-table" para cualquier objeto de datos que esté en la raíz de su jerarquía de herencia. Veamos un ejemplo:
Worker.java
import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.Inheritance; import javax.jdo.annotations.InheritanceStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; @PersistenceCapable @Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE) public abstract class Worker { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private String department; }
Employee.java
// ... imports ... @PersistenceCapable public class Employee extends Worker { @Persistent private int salary; }
Intern.java
import java.util.Date; // ... imports ... @PersistenceCapable public class Intern extends Worker { @Persistent private Date internshipEndDate; }
En este ejemplo, hemos añadido una anotación @Inheritance
a la declaración de la clase Worker
con el atributo strategy>
definido como InheritanceStrategy.SUBCLASS_TABLE
. Esto indica a JDO que almacene todos los campos persistentes de Worker
en las entidades del almacén de datos de sus subclases. La entidad de almacén de datos creada como resultado de llamar a makePersistent()
con una instancia de Employee
tiene dos propiedades llamadas "department" y "salary". La entidad de almacén de datos creada como resultado de llamar a makePersistent()
con una instancia de Intern
tendrá dos propiedades llamadas "department" y "internshipEndDate". El almacén de datos no contiene ninguna entidad de tipo "Worker".
Ahora vamos a hacer las cosas un poco más interesantes. Supongamos que, además de tener Employee
y Intern
, también queremos una especialización de Employee
que describa a los empleados que han dejado la empresa:
FormerEmployee.java
import java.util.Date; // ... imports ... @PersistenceCapable @Inheritance(customStrategy = "complete-table") public class FormerEmployee extends Employee { @Persistent private Date lastDay; }
En este ejemplo, hemos añadido una anotación @Inheritance
a la declaración de clase FormerEmployee
con su atributo custom-strategy>
definido como "complete-table". Esto indica a JDO 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 makePersistent()
con una instancia de FormerEmployee
tendrá tres propiedades llamadas "department", "salary" y "lastDay". No hay ninguna entidad de tipo "Employee" que se corresponda con un FormerEmployee
. Sin embargo, si llamas a makePersistent()
con un objeto cuyo tipo de tiempo de ejecución es Employee
, creas una entidad de tipo "Employee".
La combinación de relaciones y de herencia es eficaz siempre que los tipos declarados de los campos de tu relación coincidan con los tipos de tiempo de ejecución de los objetos que asignas a dichos campos. Para obtener más información, consulta la sección sobre relaciones polimórficas.