Relaciones de entidades en JDO

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

Las relaciones sin propietario no son compatibles con la versión 1.0 del complemento DataNucleus para App Engine, pero puedes administrar estas relaciones si almacenas claves de almacén de datos directamente en los campos. App Engine crea entidades relacionadas en grupos de entidades automáticamente para poder actualizar juntos objetos relacionados. Pero es la aplicación la que debe saber cuándo usar las transacciones del almacén de datos.

La versión 2.x del complemento DataNucleus para App Engine admite relaciones sin propietario con una sintaxis natural. La sección Relaciones sin propietario explica cómo crear estas relaciones en cada versión de complemento. A fin de actualizarse a la versión 2.x del complemento DataNucleus para App Engine, consulta Migra a la versión 2.x del complemento DataNucleus para App Engine.

Relaciones de propiedad uno a uno

Creas una relación de propiedad uno a uno unidireccional entre dos objetos persistentes con un campo cuyo tipo es la clase de la clase relacionada.

El ejemplo a continuación define una clase de datos ContactInfo y una clase de datos Employee con una relación uno a uno desde 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 distintivas en el almacén de datos con dos tipos diferentes. Las relaciones se representan con una relación de grupo de entidad: la clave del secundario usa la clave del principal como su grupo de entidad principal. Cuando una aplicación accede al objeto secundario mediante el campo del objeto principal, la implementación JDO realiza una consulta al grupo de entidad principal para obtener el secundario.

La clase secundaria debe tener un campo de clave cuyo tipo pueda contener la información de clave del principal, ya sea esta una clave o un valor de clave codificado como una string. Consulta Cómo crear datos: Claves para obtener información acerca de los tipos de campo de clave.

Creas una relación uno a uno bidireccional mediante el uso de campos en ambas clases, con una anotación en el 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 = "...", en el que el valor es el nombre del campo en la clase superior. Si el campo en un objeto se propaga, el campo de referencia correspondiente en el otro objeto se propaga de forma automática.

ContactInfo.java

import Employee;

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

Los objetos secundarios se cargan desde el almacén de datos cuando se los accede por primera vez. Si no accedes al objeto secundario en un objeto principal, la entidad para el objeto secundario nunca se carga. Si deseas cargar el elemento secundario, puedes “tocarlo” antes de cerrar el PersistenceManager (p. ej., si llamas a getContactInfo() en el ejemplo anterior) o agregar el campo secundario de forma explícita al grupo de recuperación predeterminado para que se recupere y cargue con el superior:

Employee.java

import ContactInfo;

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

Relaciones de propiedad uno a varios

Para crear una relación uno a varios 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 uno a varios es similar a una relación uno a uno, con un campo en la clase superior que usa la anotación @Persistent(mappedBy = "..."), en la que 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 colecciones enumerados en Define clases de datos: Colecciones son compatible con relaciones uno a varios. Sin embargo, los arreglos no son compatibles con relaciones uno a varios.

App Engine no admite unir consultas: no puedes consultar una entidad principal con un atributo de una entidad secundaria. (Puedes consultar una propiedad de una clase incorporada porque las clases incorporadas almacenan propiedades en la entidad principal). Consulta Cómo definir clases de datos: clases incorporadas).

Cómo mantienen el orden las colecciones ordenadas

Las colecciones ordenadas, como List<...>, preservan el orden de los objetos cuando se guarda el objeto superior. JDO requiere que la base de datos preserve este orden mediante el almacenamiento del posicionamiento de cada objeto como una propiedad del objeto. App Engine almacena esto como una propiedad de la entidad correspondiente mediante un nombre de propiedad igual al nombre del campo del superior seguido de _INTEGER_IDX. Las propiedades de posición son ineficientes. Si se agrega, quita o mueve un elemento de la colección, se deben actualizar todas las entidades subsecuentes al lugar modificado en la colección. Esto puede resultar lento y propenso a errores si no se realiza en una transacción.

Si no necesitas preservar un orden arbitrario en una colección, pero necesitas usar un tipo de colección ordenado, puedes especificar un orden según las propiedades de los elementos mediante una anotación, que es una extensión a 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 (mediante la extensión list-ordering) especifica el orden deseado de los elementos de la colección como una cláusula de ordenamiento de JDOQL. El ordenamiento usa valores de propiedades de los elementos. Como con las consultas, todos los elementos de una colección deben tener valores para las propiedades que se usan en la cláusula de ordenamiento.

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

Si deseas mayor eficiencia, siempre usa una cláusula de ordenamiento explícito para relaciones uno a varios de los tipos de colecciones ordenadas, si es posible.

Relaciones sin propietario

Además de las relaciones de propiedad, la API JDO también proporciona un servicio para gestionar relaciones sin propietario. Este servicio trabaja de otra manera según la versión del complemento DataNucleus de App Engine que utilizas:

  • La versión 1 del complemento DataNucleus no implementa relaciones sin propietario mediante una sintaxis natural, pero sí puedes administrar estas relaciones mediante valores Key en lugar de instancias (o colecciones de instancias) de tus objetos modelos. Puedes almacenar objetos de clave a medida que modelas una “clave externa” entre dos objetos. El almacén de datos no garantiza integridad referencial con estas referencias de clave, pero el uso de claves facilita modelar (y, luego, recuperar) cualquier relación entre dos objetos.

    Sin embargo, si optas por este camino, debes asegurarte de que las claves sean del tipo apropiado. JDO y el compilador no verifican tipos de Key por ti.
  • La versión 2.x del complemento DataNucleus implementa relaciones sin propietario con una sintaxis natural.

Sugerencia: en algunos casos, puede que sea necesario modelar una relación de propiedad como si fuese sin propietario. Esto ocurre porque todos los objetos involucrados en una relación de propiedad se ubican automáticamente en el mismo grupo de entidad y un grupo de entidad solo puede admitir de una a diez escrituras por segundo. Por ejemplo, si un objeto principal recibe 0.75 escrituras por segundo y un objeto secundario está recibiendo 0.75 escrituras por segundos, puede que sea razonable modelar la relación como una sin propietario de modo que tanto el objeto principal como el secundario residan en sus grupos de entidades independientes y propios.

Relaciones sin propietario uno a uno

Imagina que quieres modelar personas y comida, y 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 varias personas. Esta sección te muestra cómo hacerlo.

En JDO 2.3

En este ejemplo, le damos a Person un miembro de tipo Key, en el que 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 pertenecen al mismo grupo de entidades, no podrás actualizar esa persona ni su comida favorita en una transacción única, a menos que la configuración de JDO permita 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 darle a Person una clave que represente su comida favorita, creamos un miembro privado del 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 uno a varios sin propietario

Ahora imagina que quieres dejar que una persona tenga varias comidas favoritas. Una vez más, una comida favorita no pertenece a la persona porque puede ser la comida favorita de varias personas.

En JDO 2.3

En este ejemplo, en lugar de darle a la persona un miembro del tipo Set<Food> para representar sus alimentos favoritos, le damos un miembro del tipo Set<Key>, en el que el conjunto contiene los identificadores únicos de objetos Food. Ten en cuenta que, si una instancia de Person y una de Food que se encuentran en Person.favoriteFoods no están en el mismo grupo de entidades, debes establecer su configuración de JDO para habilitar las transacciones entre grupos (XG) si deseas 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 la persona un miembro del tipo Set<Food>, en el que el conjunto representa sus alimentos favoritos.

Person.java

// ... imports ...

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

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

Relaciones varios a varios

Para poder modelar relaciones varios a varios, se deben mantener colecciones de claves en ambos lados de la relación. En los ejemplos a continuación, Food realiza un seguimiento de las personas que la consideran su favorita.

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 la consideran su favorita.

Cuando se modela un valor de varios a varios mediante valores Key, ten en cuenta que es responsabilidad de la app 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 de Food que se encuentran en Person.favoriteFoods no están en el mismo grupo de entidades y deseas actualizarlas en una sola transacción, debes establecer tu configuración de JDO para habilitar 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, se guardan automáticamente todos los demás objetos a los que se puede llegar mediante relaciones y deben guardarse (ya que son nuevos o se han modificado desde que se cargaron). Esto tiene implicaciones importantes para las transacciones y los grupos de entidades.

Considera el siguiente ejemplo mediante 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 el objeto Employee nuevo se guarda mediante el método pm.makePersistent(), el objeto nuevo ContactInfo relacionado se guarda de forma automática. Debido a que ambos objetos son nuevos, App Engine crea dos entidades nuevas en el mismo grupo de entidades y usa la entidad Employee como superior de la entidad ContactInfo. Del mismo modo, si el objeto Employee ya se guardó y el objeto ContactInfo relacionado es nuevo, App Engine crea la entidad ContactInfo mediante el uso de la entidad Employee existente como el superior.

Sin embargo, ten en cuenta que la llamada a pm.makePersistent() en este ejemplo no usa una transacción. Sin una transacción explícita, ambas entidades se crean con acciones atómicas separadas. En este caso, es posible que la creación de la entidad Employee se realice correctamente, pero no la creación de la entidad ContactInfo. Para asegurarse de que ambas entidades se crean correctamente o que ninguna se cree, 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 la relación se estableciera, App Engine no podrá “mover” la entidad ContactInfo existente al grupo de entidades de la entidad Employee porque 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 borrar en la misma transacción si estableces la configuración de JDO en habilitar transacciones entre grupos (XG). Si no usas transacciones XG y tratas de actualizar o borrar las entidades de grupos diferentes en la misma transacción, se generará una excepción JDOFatalUserException.

Si guardas un objeto principal cuyos objetos secundarios se modificaron, se guardarán los cambios en el objeto secundario. Es aconsejable permitir que los objetos principales mantengan persistencia para todos los objetos secundarios relacionados de esta manera, y usar transacciones cuando se guardan los cambios.

Objetos secundarios dependientes y eliminaciones en cascada

Una relación de propiedad puede ser "dependiente", es decir, que el objeto secundario no existe sin el principal. Si una relación es dependiente y se borra un objeto principal, todos los objetos secundarios también se borran. Otra forma de borrar un objeto secundario antiguo es romper una relación de propiedad dependiente mediante la asignación de un valor nuevo al campo dependiente en el objeto principal. Puedes declarar que una relación de propiedad uno a uno es dependiente si agregas dependent="true" a la anotación Persistent del campo en el objeto superior que hace referencia al objeto secundario:

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

Puedes declarar que una relación de propiedad uno a varios es dependiente si agregas una anotación @Element(dependent = "true") al campo en el objeto superior que hace referencia a la colección secundaria:

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

Como en la creación y actualización de objetos, si necesitas que todas las eliminaciones en una eliminación en cascada ocurran en una acción atómica única, debes realizar la eliminación en una transacción.

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

Relaciones polimorfas

Incluso si la especificación JDO es compatible con relaciones polimorfas, las relaciones polimorfas no son compatibles en la implementación DO de App Engine. Esta es una limitación que esperamos poder quitar en actualizaciones futuras del producto. Si necesitas referirte a varios tipos de objetos mediante una clase de base común, recomendamos que se use la misma estrategia para implementar relaciones sin propietario: almacenar una referencia de clave. Por ejemplo, si tienes una clase base Recipe con las especializaciones Appetizer, Entree y Dessert, y deseas modelar la Recipe favorita 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;
}

Por desgracia, si creas una instancia de Entree y la asignas al objeto Chef.favoriteRecipe, recibirás una UnsupportedOperationException cuando intentes conservar el objeto Chef. Esto se debe a que el tipo de entorno de ejecución del objeto, Entree, no coincide con el tipo declarado del campo de relación, Recipe. La solución alternativa es cambiar el tipo de Chef.favoriteRecipe de un objeto Recipe a un objeto Key:

Chef.java

// ... imports ...

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

    @Persistent
    private Key favoriteRecipe;
}

Dado que Chef.favoriteRecipe ya no es un campo de relación, puedes hacer referencia a un objeto de cualquier tipo. Como con una relación sin propietario, la desventaja es que necesitas gestionar esta relación de manera manual.