Relaciones de entidad en JDO

Puedes modelar relaciones entre objetos persistentes con campos del tipo 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. Para actualizarse a la versión 2.x del complemento DataNucleus de App Engine, consulta Cómo migrar 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 con 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 principal. Si el campo en un objeto se propaga, el campo de referencia correspondiente en el otro objeto se propaga 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 los accede por primera vez. Si no accedes a un objeto secundario desde un objeto principal, la entidad para el objeto secundario nunca se carga. Si quieres cargar el secundario, puedes "tocarlo" antes de cerrar PersistenceManager (p. ej.: si llamas getContactInfo() en el ejemplo anterior) o agregar explícitamente el campo secundario al grupo de actualización de información predeterminada, de manera que se recupere y se cargue con el principal.

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 Collection de la clase relacionada:

Employee.java

import java.util.List;

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

Una relación uno a varios bidireccional es similar a una relación uno a uno, con el campo en la clase principal con la anotación @Persistent(mappedBy = "..."), en el 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 Cómo definir clases de datos: Colecciones son compatible con relaciones uno a varios. Sin embargo, los arreglos no son compatible 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 el objeto principal se guarda. JDO requiere que la base de datos preserve este orden mediante el almacenamiento del posicionamiento de cada objeto como una propiedad de un objeto. App Engine almacena esto como una entidad de la entidad correspondiente con un nombre de propiedad igual al nombre del campo del principal seguido de _INTEGER_IDX. Las propiedades de posicionamiento 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 un colección, pero necesitas usar un tipo de colección ordenado, puedes especificar un orden según las propiedades de los elementos con 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 (con 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 utiliza 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 con una sintaxis natural, pero sí puedes gestionar las relaciones con 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, debe asegurarte de que las claves sean de los tipos apropiados. JDO y el compilador no registran 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, se le da Person a 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 al que se refiere Person.favoriteFood no están en el mismo grupo de entidad, no puedes actualizar la persona y la comida favorita de esa persona en una única transacción, a menos que tu configuración JDO esté establecida en habilitar 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 dar a Person una clave que representa su comida favorita, se crea 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 dar a Person un miembro de tipo Set<Food> para representar la comida favorita de la persona, se le da a Person un miembro de tipo Set<Key>, donde el definidor contiene los identificadores únicos de los objetos Food. Ten en cuenta que si una instancia de Person y una instancia de Food contenida en Person.favoriteFoods no están en el mismo grupo de entidad, debes establecer tu configuración de JDO en habilitar transacciones entre grupos (XG) si quieres actualizarlos 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, se le da Person a un miembro de tipo Set<Food> en el que el conjunto representa las comidas favoritas de las personas.

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. Los ejemplos a continuación son diferentes: Food realiza un seguimiento de las personas que la consideran 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 manera única los objetos Food que son favoritos, y Food mantiene el conjunto de valores Key que identifican de manera única los objetos Person que la consideran favorita.

Cuando modelas varios a varios con valores Key, ten en cuenta que es la aplicación la que mantiene 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 entidad y quieres actualizarlos en una única transacción, debes establecer tu configuración de JDO en habilitar transacciones entre grupos (XG).

Relaciones, grupos de entidad 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 entidad.

Considera el ejemplo a continuación con relación unidireccional entre las clases de 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 con el método pm.makePersistent(), el objeto ContactInfo relacionado nuevo se guarda automáticamente. Debido a que ambos objetos son nuevos, App Engine crea dos entidades nuevas en el mismo grupo de entidad, con la entidad Employee como la entidad principal de ContactInfo. De igual forma, si el objeto Employee se guardó y el objeto relacionado ContactInfo es nuevo, App Engine crea la entidad ContactInfo con la entidad Employee existente como principal.

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 guardan antes de que la relación se establezca, App Engine no puede "mover" la entidad ContactInfo existente al grupo de entidad de las entidades Employee, porque los grupos de entidad 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 tu configuración de JDO en habilitar transacciones entre grupos (XG). Si no usas transacciones XG y tratas de actualizar o borrar entidades de grupos diferentes en la misma transacción, verás una 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 sea dependiente si agregas dependent="true" a la anotación Persistent del campo en el objeto principal que se refiere al objeto secundario:

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

Puedes declarar que una relación de propiedad de uno a varios sea dependiente si agregas una anotación @Element(dependent = "true") al campo en el objeto principal que se refiere 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 funciona para borrar objetos secundarios dependientes, pero no el almacén de datos. Si borras una entidad principal con la API de bajo nivel o Google Cloud Platform Console, el objeto secundario relacionado no se borra.

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 la misma estrategia se use para implementar relaciones sin propietario: almacenar una referencia Key. Por ejemplo, si tienes una clase de base Recipe con especializaciones Appetizer, Entree y Dessert. y quieres modelar la Recipe favorita de un Chef, puedes modelarla 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;
}

Desafortunadamente, si creas una nueva instancia de Entree y la asignas a Chef.favoriteRecipe, obtendrás una UnsupportedOperationException cuando tratas de persistir el objeto Chef. Esto sucede porque 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 desde una Recipe a Key:

Chef.java

// ... imports ...

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

    @Persistent
    private Key favoriteRecipe;
}

Debido a que Chef.favoriteRecipe ya no es un campo de relación, puede referirse a un objeto de cualquier tipo. Como con una relación sin propietario, una desventaja es que necesitas gestionar esta relación de manera manual.

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Entorno estándar de App Engine para Java