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. 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 deKey
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 ListcontactInfos;
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 la consola de Google Cloud, 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.