Relaciones de entidades en JDO

Puede modelar relaciones entre objetos persistentes mediante campos de los tipos de objeto. Una relación entre objetos persistentes se puede describir como propiedad, donde uno de los objetos no puede existir sin el otro, o sin propiedad, donde ambos objetos pueden existir independientemente de su relación entre sí. La implementación de la interfaz JDO de App Engine puede modelar relaciones de uno a uno y de uno a muchos, tanto unidireccionales como bidireccionales, con y sin propietario.

Las relaciones no propias no se admiten en la versión 1.0 del complemento DataNucleus para App Engine, pero puedes gestionar estas relaciones tú mismo almacenando claves de Datastore directamente en los campos. App Engine crea automáticamente entidades relacionadas en grupos de entidades para admitir la actualización conjunta de objetos relacionados, pero es responsabilidad de la aplicación saber cuándo usar las transacciones de Datastore.

La versión 2.x del complemento DataNucleus para App Engine admite relaciones no propias con una sintaxis natural. En la sección Relaciones sin propietario se explica cómo crear relaciones sin propietario en cada versión del complemento. Para actualizar a la versión 2.x del complemento DataNucleus para App Engine, consulta Migrar a la versión 2.x del complemento DataNucleus para App Engine.

Relaciones de propiedad de uno a uno

Para crear una relación de propiedad unidireccional de uno a uno entre dos objetos persistentes, usa un campo cuyo tipo sea la clase de la clase relacionada.

En el siguiente ejemplo se definen una clase de datos ContactInfo y una clase de datos Employee, con una relación uno a uno de Employee a 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;

    // ...
}

Employee.java

import ContactInfo;
// ... imports ...

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private ContactInfo contactInfo;

    ContactInfo getContactInfo() {
        return contactInfo;
    }
    void setContactInfo(ContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

    // ...
}

Los objetos persistentes se representan como dos entidades distintas en el almacén de datos, con dos tipos diferentes. La relación se representa mediante una relación de grupo de entidades: la clave del elemento secundario usa la clave del elemento superior como elemento superior del grupo de entidades. Cuando la aplicación accede al objeto secundario mediante el campo del objeto principal, la implementación de JDO realiza una consulta principal del grupo de entidades para obtener el objeto secundario.

La clase secundaria debe tener un campo de clave cuyo tipo pueda contener la información de la clave principal: un objeto Key o un valor Key codificado como una cadena. Consulta Crear datos: claves para obtener información sobre los tipos de campos de clave.

Para crear una relación bidireccional de uno a uno, se usan campos de ambas clases. Se añade una anotación al campo de la clase secundaria para declarar que los campos representan una relación bidireccional. El campo de la clase secundaria debe tener una anotación @Persistent con el argumento mappedBy = "...", donde el valor es el nombre del campo de la clase principal. Si se rellena el campo de un objeto, el campo de referencia correspondiente del otro objeto se rellena automáticamente.

ContactInfo.java

import Employee;

// ...
    @Persistent(mappedBy = "contactInfo")
    private Employee employee;

Los objetos secundarios se cargan desde el almacén de datos cuando se accede a ellos por primera vez. Si no accedes al objeto secundario de un objeto principal, la entidad del objeto secundario nunca se carga. Si quieres cargar el elemento secundario, puedes "tocarlo" antes de cerrar PersistenceManager (por ejemplo, llamando a getContactInfo() en el ejemplo anterior) o añadir explícitamente el campo secundario al grupo de obtención predeterminado para que se recupere y se cargue con el elemento principal:

Employee.java

import ContactInfo;

// ...
    @Persistent(defaultFetchGroup = "true")
    private ContactInfo contactInfo;

Relaciones de propiedad de uno a varios

Para crear una relación de uno a muchos entre objetos de una clase y varios objetos de otra, usa una colección de la clase relacionada:

Employee.java

import java.util.List;

// ...
    @Persistent
    private List<ContactInfo> contactInfoSets;

Una relación bidireccional de uno a muchos es similar a una de uno a uno, con un campo en la clase principal que usa la anotación @Persistent(mappedBy = "..."), donde el valor es el nombre del campo en la clase secundaria:

Employee.java

import java.util.List;

// ...
    @Persistent(mappedBy = "employee")
    private List<ContactInfo> contactInfoSets;

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

Los tipos de colección que se indican en Definir clases de datos: colecciones se admiten en relaciones de uno a muchos. Sin embargo, las matrices no se admiten en las relaciones de uno a muchos.

App Engine no admite consultas de unión: no puedes consultar una entidad principal con un atributo de una entidad secundaria. Puedes consultar una propiedad de una clase insertada porque las clases insertadas almacenan propiedades en la entidad principal. Consulta Definición de clases de datos: clases insertadas.

Mantenimiento del orden en colecciones ordenadas

Las colecciones ordenadas, como List<...>, conservan el orden de los objetos cuando se guarda el objeto principal. JDO requiere que las bases de datos conserven este orden almacenando la posición de cada objeto como una propiedad del objeto. App Engine almacena este valor como una propiedad de la entidad correspondiente, con un nombre de propiedad igual al nombre del campo del elemento superior seguido de _INTEGER_IDX. Las propiedades de posición son ineficientes. Si se añade, se quita o se mueve un elemento en la colección, se deben actualizar todas las entidades posteriores al lugar modificado de la colección. Este proceso puede ser lento y propenso a errores si no se realiza en una transacción.

Si no necesitas conservar un orden arbitrario en una colección, pero sí usar un tipo de colección ordenada, puedes especificar un orden basado en las propiedades de los elementos mediante una anotación, una extensión de JDO proporcionada por DataNucleus:

import java.util.List;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    @Order(extensions = @Extension(vendorName="datanucleus",key="list-ordering", value="state asc, city asc"))
    private List<ContactInfo> contactInfoSets = new ArrayList<ContactInfo>();

La anotación @Order (que usa la extensión list-ordering ) especifica el orden deseado de los elementos de la colección como una cláusula de ordenación de JDOQL. Para la ordenación se utilizan los valores de propiedad de los elementos. Al igual que con las consultas, todos los elementos de una colección deben tener valores en las propiedades utilizadas en la cláusula de ordenación.

Al acceder a una colección se realiza una consulta. Si la cláusula de ordenación de un campo usa más de un orden de clasificación, la consulta requiere un índice de Datastore. Consulta la página Índices de Datastore para obtener más información.

Para mejorar la eficiencia, siempre que sea posible, utilice una cláusula de orden explícita para las relaciones de uno a varios de los tipos de colección ordenada.

Relaciones sin propiedad

Además de las relaciones de propiedad, la API JDO también proporciona una función para gestionar relaciones sin propietario. Esta función funciona de forma diferente según la versión del complemento DataNucleus para App Engine que estés usando:

  • La versión 1 del complemento DataNucleus no implementa relaciones no propias mediante una sintaxis natural, pero puedes gestionar estas relaciones usando valores Key en lugar de instancias (o colecciones de instancias) de tus objetos de modelo. Puedes pensar en almacenar objetos Key como si se tratara de una "clave externa" arbitraria entre dos objetos. El almacén de datos no garantiza la integridad referencial con estas referencias de clave, pero el uso de Key facilita mucho la modelización (y la obtención) de cualquier relación entre dos objetos.

    Sin embargo, si sigues este método, debes asegurarte de que las claves sean del tipo adecuado. JDO y el compilador no comprueban los tipos de Key por ti.
  • La versión 2.x del complemento DataNucleus implementa relaciones no propias mediante una sintaxis natural.

Nota: En algunos casos, puede que tengas que modelar una relación de propiedad como si no lo fuera. Esto se debe a que todos los objetos implicados en una relación de propiedad se colocan automáticamente en el mismo grupo de entidades y un grupo de entidades solo puede admitir entre una y diez escrituras por segundo. Por ejemplo, si un objeto principal recibe 0,75 escrituras por segundo y un objeto secundario recibe 0, 75 escrituras por segundo, puede ser conveniente modelar esta relación como no propiedad para que tanto el objeto principal como el secundario residan en sus propios grupos de entidades independientes.

Relaciones sin propiedad de uno a uno

Supongamos que quieres modelar personas y comida, donde una persona solo puede tener una comida favorita, pero una comida favorita no pertenece a la persona, ya que puede ser la comida favorita de cualquier número de personas. En esta sección se explica cómo hacerlo.

En JDO 2.3

En este ejemplo, asignamos Person a un miembro de tipo Key, donde Key es el identificador único de un objeto Food. Si una instancia de Person y la instancia de Food a la que hace referencia Person.favoriteFood no están en el mismo grupo de entidades, no puedes actualizar la persona y su comida favorita en una sola transacción, a menos que tu configuración de JDO esté definida para habilitar las transacciones entre grupos (XG).

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

@PersistenceCapable
public class Food {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    // ...
}

En JDO 3.0

En este ejemplo, en lugar de asignar a Person una clave que represente su comida favorita, creamos un miembro privado de tipo Food:

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    @Unowned
    private Food favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

@PersistenceCapable
public class Food {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    // ...
}

Relaciones sin propiedad de uno a varios

Ahora queremos que una persona tenga varias comidas favoritas. De nuevo, la comida favorita no pertenece a la persona, ya que puede ser la comida favorita de cualquier número de personas.

En JDO 2.3

En este ejemplo, en lugar de asignar a Person un miembro de tipo Set<Food> para representar las comidas favoritas de la persona, le asignamos un miembro de tipo Set<Key>, donde el conjunto contiene los identificadores únicos de los objetos Food. Ten en cuenta que, si una instancia de Person y una instancia de Food contenidas en Person.favoriteFoods no están en el mismo grupo de entidades, debes definir tu configuración de JDO para habilitar las transacciones entre grupos (XG) si quieres actualizarlas en la misma transacción.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

En JDO 3.0

En este ejemplo, asignamos a Person un miembro de tipo Set<Food> donde el conjunto representa las comidas favoritas de la persona.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

Relaciones de varios a varios

Podemos modelizar una relación de muchos a muchos manteniendo colecciones de claves en ambos lados de la relación. Vamos a ajustar nuestro ejemplo para que Food lleve un registro de las personas que lo consideran favorito:

Person.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> favoriteFoods;

Food.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> foodFans;

En este ejemplo, Person mantiene un conjunto de valores Key que identifican de forma única los objetos Food que son favoritos, y Food mantiene un conjunto de valores Key que identifican de forma única los objetos Person que lo consideran favorito.

Cuando modelices una relación de muchos a muchos con valores de Key, ten en cuenta que es responsabilidad de la aplicación mantener ambos lados de la relación:

Album.java

// ...
public void addFavoriteFood(Food food) {
    favoriteFoods.add(food.getKey());
    food.getFoodFans().add(getKey());
}

public void removeFavoriteFood(Food food) {
    favoriteFoods.remove(food.getKey());
    food.getFoodFans().remove(getKey());
}

Si una instancia de Person y una instancia de Food contenida en Person.favoriteFoods no están en el mismo grupo de entidades y quieres actualizarlas en una sola transacción, debes configurar JDO para habilitar las transacciones entre grupos (XG).

Relaciones, grupos de entidades y transacciones

Cuando tu aplicación guarda un objeto con relaciones de propiedad en el almacén de datos, todos los demás objetos a los que se puede acceder a través de relaciones y que deben guardarse (son nuevos o se han modificado desde la última vez que se cargaron) se guardan automáticamente. Esto tiene implicaciones importantes para las transacciones y los grupos de entidades.

Veamos el siguiente ejemplo, en el que se usa una relación unidireccional entre las clases Employee y ContactInfo anteriores:

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    pm.makePersistent(e);

Cuando se guarda el nuevo objeto Employee con el método pm.makePersistent(), el nuevo objeto ContactInfo relacionado se guarda automáticamente. Como ambos objetos son nuevos, App Engine crea dos entidades en el mismo grupo de entidades, usando la entidad Employee como elemento superior de la entidad ContactInfo. Del mismo modo, si el objeto Employee ya se ha guardado y el objeto ContactInfo relacionado es nuevo, App Engine crea la entidad ContactInfo usando la entidad Employee como elemento superior.

Sin embargo, ten en cuenta que la llamada a pm.makePersistent() en este ejemplo no usa una transacción. Si no hay una transacción explícita, ambas entidades se crean mediante acciones atómicas independientes. En este caso, es posible que se cree la entidad Employee, pero no la entidad ContactInfo. Para asegurarte de que se creen ambas entidades correctamente o de que no se cree ninguna, debes usar una transacción:

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    try {
        Transaction tx = pm.currentTransaction();
        tx.begin();
        pm.makePersistent(e);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

Si ambos objetos se guardaron antes de que se estableciera la relación, App Engine no podrá "mover" la entidad ContactInfo al grupo de entidades de la entidad Employee, ya que los grupos de entidades solo se pueden asignar cuando se crean las entidades. App Engine puede establecer la relación con una referencia, pero las entidades relacionadas no estarán en el mismo grupo. En este caso, las dos entidades se pueden actualizar o eliminar en la misma transacción si configuras JDO para habilitar las transacciones entre grupos (XG). Si no utiliza transacciones XG, al intentar actualizar o eliminar entidades de diferentes grupos en la misma transacción, se producirá una excepción JDOFatalUserException.

Si guardas un objeto principal cuyos objetos secundarios se han modificado, se guardarán los cambios en los objetos secundarios. Es recomendable permitir que los objetos principales mantengan la persistencia de todos los objetos secundarios relacionados de esta forma y usar transacciones al guardar los cambios.

Hijos dependientes y eliminaciones en cascada

Una relación de propiedad puede ser "dependiente", lo que significa que el elemento secundario no puede existir sin su elemento superior. Si una relación es dependiente y se elimina un objeto principal, también se eliminan todos los objetos secundarios. Si rompe una relación de dependencia asignando un nuevo valor al campo dependiente del elemento principal, también se eliminará el elemento secundario antiguo. Puedes declarar que una relación de propiedad de uno a uno es dependiente añadiendo dependent="true" a la anotación Persistent del campo del objeto principal que hace referencia al secundario:

// ...
    @Persistent(dependent = "true")
    private ContactInfo contactInfo;

Puedes declarar que una relación de propiedad de uno a muchos es dependiente añadiendo una anotación @Element(dependent = "true") al campo del objeto principal que hace referencia a la colección secundaria:

import javax.jdo.annotations.Element;
// ...
    @Persistent
    @Element(dependent = "true")
    private List contactInfos;

Al igual que ocurre con la creación y la actualización de objetos, si necesitas que todas las eliminaciones de una eliminación en cascada se produzcan en una sola acción atómica, debes realizar la eliminación en una transacción.

Nota: La implementación de JDO se encarga de eliminar los objetos secundarios dependientes, no el almacén de datos. Si eliminas una entidad principal con la API de bajo nivel o la consola Google Cloud , los objetos secundarios relacionados no se eliminarán.

Relaciones polimórficas

Aunque la especificación de JDO incluye la compatibilidad con relaciones polimórficas, estas relaciones aún no se admiten en la implementación de DO de App Engine. Esperamos eliminar esta limitación en futuras versiones del producto. Si necesitas hacer referencia a varios tipos de objetos a través de una clase base común, te recomendamos que sigas la misma estrategia que se usa para implementar relaciones no propias: almacena una referencia de Key. Por ejemplo, si tienes una clase base Recipe con las especializaciones Appetizer, Entree y Dessert, y quieres modelizar el Recipe favorito de un Chef, puedes hacerlo de la siguiente manera:

Recipe.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 Recipe {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private int prepTime;
}

Appetizer.java

// ... imports ...

@PersistenceCapable
public class Appetizer extends Recipe {
// ... appetizer-specific fields
}

Entree.java

// ... imports ...

@PersistenceCapable
public class Entree extends Recipe {
// ... entree-specific fields
}

Dessert.java

// ... imports ...

@PersistenceCapable
public class Dessert extends Recipe {
// ... dessert-specific fields
}

Chef.java

// ... imports ...

@PersistenceCapable
public class Chef {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent(dependent = "true")
    private Recipe favoriteRecipe;
}

Lamentablemente, si creas una instancia de Entree y la asignas a Chef.favoriteRecipe, obtendrás un UnsupportedOperationException cuando intentes conservar el objeto Chef. Esto se debe a que el tipo de tiempo de ejecución del objeto, Entree, no coincide con el tipo declarado del campo de relación, Recipe. Para solucionar este problema, cambia el tipo de Chef.favoriteRecipe de Recipe a Key:

Chef.java

// ... imports ...

@PersistenceCapable
public class Chef {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Key favoriteRecipe;
}

Como Chef.favoriteRecipe ya no es un campo de relación, puede hacer referencia a un objeto de cualquier tipo. El inconveniente es que, al igual que con una relación sin propiedad, debes gestionar esta relación manualmente.