Define clases de datos con JDO

Puedes usar JDO para almacenar objetos de datos Java simples (también conocidos como "Objetos Java antiguos y simples" o "POJO") en el almacén de datos. Cada objeto que se vuelve persistente con el PersistenceManager se convierte en una entidad en el almacén de datos. Usa las anotaciones para indicar 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 Java. Estas aún funcionan con JDO 2.3. En este documento, solo se habla sobre el uso de las anotaciones Java con clases de datos.

Anotaciones de clase y campo

Cada objeto que JDO guarda se convierte en una entidad del almacén de datos de App Engine. El tipo de la entidad 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. El nombre de la propiedad es el mismo que el del campo (con distinción de mayúsculas y minúsculas).

Para declarar que una clase Java es capaz de almacenarse y recuperarse del almacén de datos con JDO, aplícale 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 se deben almacenar en el almacén de datos se deben declarar 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 un campo como no persistente (no se almacena en el almacén de datos y no se restablece cuando se recupera el objeto), asígnale una anotación @NotPersistent.

Sugerencia: JDO especifica que, de forma predeterminada, los campos de ciertos tipos son persistentes si no se especifican las anotaciones @Persistent o @NotPersistent y los campos de los otros tipos no son persistentes de forma predeterminada. Consulta la documentación de DataNucleus para obtener una descripción completa de este comportamiento. Debido a que, según la especificación de JDO, no todos los tipos de valores principales del almacén de datos de App Engine son persistentes de forma predeterminada, recomendamos que anotes de manera explícita los campos como @Persistent o @NotPersistent para mayor claridad.

El tipo de un campo puede ser cualquiera de los que se detallan a continuación.

  • Uno de los tipos principales compatibles con el almacén de datos
  • una Colección (como java.util.List<...>) o un arreglo 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 integrada almacenada como propiedades en la entidad

Una clase de datos debe tener solo 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 tipo de campo de clave más flexible es un objeto Key que JDO propaga de forma 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 primarias del tipo Key requieren una anotación @PrimaryKey y una anotación @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY):

Sugerencia: Haz que todos tus campos persistentes sean private o protected (o que estén protegidos por paquetes) y solo proporciona acceso público a través de métodos de acceso. El acceso directo a un campo persistente desde otra clase puede omitir la mejora de la clase JDO. Como alternativa, 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;

Aquí tienes un ejemplo de una 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 valores principales

Para representar una propiedad que contiene un valor único 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, si se almacena el valor serializado de la instancia en un único valor de propiedad del tipo Blob. El campo usa la anotación @Persistent(serialized=true) para indicar a JDO que serialice el valor. Los valores Blob no se indexan y no se pueden usar ni en filtros de consulta ni en órdenes de clasificación.

A continuación, se muestra un ejemplo de una clase serializable simple que representa un archivo, e incluye el contenido del archivo, su nombre y el tipo MIME. Esta no es 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 un 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;

Relaciones y objetos secundarios

Un valor de campo que es una instancia de una clase @PersistenceCapable crea una relación de propiedad uno a uno entre dos objetos. Un campo que sea una colección de esas referencias crea una relación de propiedad uno a varios.

Importante: Las relaciones de propiedad tienen implicaciones importantes para las transacciones, los grupos de entidad y las eliminaciones en cascada. Consulta Transacciones y Relaciones para obtener más información.

A continuación, se muestra un ejemplo simple de una relación de propiedad uno a uno entre un objeto Employee y un objeto 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 app crea una instancia de Employee, propaga el campo myContactInfo con una instancia ContactInfo nueva y, luego, guarda la instancia Employee con pm.makePersistent(...), el almacén de datos creará dos entidades. Una es del tipo "ContactInfo", que representa la instancia ContactInfo. El otro es del tipo "Employee". La clave de la entidad ContactInfo tiene la clave de la entidad Employee como su grupo de entidad principal.

Clases incorporadas

Las clases incorporadas te permiten dar forma a un valor de campo con una clase sin crear una entidad de almacén de datos nueva y formar una relación. Los campos del valor de objeto se almacenan directo en la entidad del almacén de datos para contener un objeto.

Cualquier clase de datos @PersistenceCapable se puede usar como objeto incorporado en otra clase de datos. Los campos de la clase @Persistent se incorporan en el objeto. Si le indicas a una clase que incorpore la anotación @EmbeddedOnly, la clase solo se podrá usar como una clase incorporada. La clase incorporada no necesita un campo de clave primaria porque no se almacena como una entidad separada.

A continuación, se muestra un ejemplo de una clase incorporada. Este ejemplo convierte la clase incorporada en una clase interna de la clase de datos que la usa, lo que es útil, pero no necesario para incorporar una clase.

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 incorporada se almacenan como propiedades en la entidad con el nombre de cada campo y el de la propiedad correspondiente. Si tienes más de un campo en el objeto cuyo tipo es una clase incorporada, debes cambiar el nombre de los campos de uno de ellos para que no entren en conflicto con otro. Especifica nombres de campo nuevos con los 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;

De manera similar, los campos en el objeto no deben usar nombres que entren en conflicto con los campos de clases incorporadas, a menos que se cambie el nombre de los campos incorporados.

Debido a que las propiedades persistentes de las clases incorporadas se almacenan en la misma entidad que los otros campos, puedes usar campos persistentes de la clase incorporada en los filtros de consulta JDOQL y las órdenes de clasificación. Puedes referirte al campo incorporado con el nombre del campo exterior, un punto (..) y el nombre del campo incorporado. Esto funciona a pesar de que los nombres de la propiedad de los campos incorporados se hayan cambiado con las anotaciones @Column.

    select from EmployeeContacts where workContactInfo.zipCode == "98105"

Colecciones

Una propiedad del almacén de datos puede tener más de un valor. En JDO, se representa con un único campo mediante un tipo de colección en el que la colección es uno de los tipos de valores principales o una clase serializable. Los siguientes tipos de colección son compatibles:

  • 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 una Lista, los objetos que muestra el almacén de datos tienen un valor ArrayList. Si un campo se declara como un Conjunto, el almacén de datos muestra un HashSet. Si un campo se declara como un conjunto ordenado SortedSet, el almacén de datos muestra un TreeSet.

Por ejemplo, un campo cuyo tipo es List<String> se almacena como cero o más valores de string para 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 varios. Consulta Relaciones.

Las propiedades del almacén de datos con más de un valor tienen un comportamiento especial con los filtros de consulta y los pedidos de clasificación. Visita la página Consultas del almacén de datos para obtener más información.

Campos de objeto y propiedades de la 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: cada campo de un objeto tiene un valor, posiblemente null. Si un campo con un tipo de valor que acepta valores nulos (algo distinto de un tipo integrado como int o boolean) se establece en null, cuando se guarda el objeto, la entidad resultante tendrá la propiedad establecida con un valor nulo.

Si una entidad del almacén de datos se carga en un objeto, no tiene una propiedad para uno de los campos del objeto y el tipo del campo es un tipo de valor único que acepta valores nulos, el campo se establece como null. Cuando el objeto se vuelve a guardar en el almacén de datos, la propiedad null se establece en el almacén de datos con valor nulo. Si el campo no es un tipo de valor que acepte valores nulos, cuando se cargue una entidad sin la propiedad correspondiente, se generará una excepción. Esto no sucede si la entidad se creó a partir de la misma clase de JDO que se usó para recrear la instancia, pero puede suceder si la clase JDO cambia o si la entidad se creó con una API de bajo nivel en lugar de JDO.

Si el tipo de 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 mediante el establecimiento de la propiedad en un único valor nulo. Si el tipo del campo es un tipo de arreglo, se le asigna un arreglo de 0 elementos. Si el objeto está cargado y no hay valor para la propiedad, se asigna al campo una colección vacía del tipo adecuado. A nivel interno, el almacén de datos distingue entre una colección vacía y una que contiene un valor nulo.

Si la entidad tiene una propiedad sin un campo correspondiente en el objeto, no se podrá acceder a esta desde el objeto. Si se vuelve a guardar el objeto en el almacén de datos, la propiedad adicional se borra.

Si una entidad tiene una propiedad cuyo valor es de un tipo diferente al del campo correspondiente en el objeto, JDO intenta transformar el valor en el tipo del campo. Si eso no es posible, JDO muestra una ClassCastException. En el caso de los números (enteros largos y flotantes de doble ancho), el valor no se transforma, sino que se convierte. Si el valor de la propiedad numérica es mayor que el tipo de campo, la conversión desborda sin mostrar una excepción.

Para indicar que una propiedad no está indexada, agrega la línea

    @Extension(vendorName="datanucleus", key="gae.unindexed", value="true")

arriba de la propiedad en la definición de la clase. Consulta la sección Propiedades no indexadas de los documentos principales para obtener información adicional sobre qué significa que una propiedad no sea indexada.

Herencia

Es normal crear clases de datos que usen la herencia y JDO lo admite. 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 luego regreses a esta página. ¿Listo? Muy bien. La herencia de JDO 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 "new-table" te permite dividir los datos de un objeto de datos único en varias "tablas". Sin embargo, como 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 puede ser muy ineficiente, por lo que la estrategia de herencia "new-table" no es compatible con 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 "superclass-table" te permite almacenar los datos de un objeto de datos en la "tabla" de su superclase. 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 "subclass-table" y "complete-table" funcionan como se describe en la documentación de DataNucleus y también 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, agregamos una anotación @Inheritance a la declaración de clase Worker con su atributo strategy> establecido en InheritanceStrategy.SUBCLASS_TABLE. Esto indica a JDO 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 makePersistent() 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 makePersistent() con una instancia Intern tendrá dos propiedades llamadas "department" y "inernshipEndDate". El almacén de datos no contiene ninguna entidad del tipo "Worker".

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;
// ... imports ...

@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class FormerEmployee extends Employee {
    @Persistent
    private Date lastDay;
}

En este ejemplo, agregamos una anotación @Inheritance a la declaración de la clase FormerEmployee con su atributo custom-strategy> establecido en "complete-table". Esto indica a JDO 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 makePersistent() con una instancia FormerEmployee tendrá tres propiedades llamadas "department", "salary" y "lastDay". Ninguna entidad del tipo "Employee" corresponde a una FormerEmployee. Sin embargo, si llamas a makePersistent() con un objeto cuyo entorno de ejecución es Employee, crearás una entidad de tipo "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.