Permalink
Browse files

Add bidirectional association support for embedded entities.

* Modify test cases to check inverse links work for one-to-many and one-to-one.
* Modify mapping strategy class to use same bidirectional code for embedded and non-embedded.
* Prevent infinite recursion saving embedded entities with bidirectional associations.
* Implement code to fix-up back-links when loading embedded entities.
  • Loading branch information...
1 parent 14cb744 commit 7bf24d05cb303d53532ee0c857a686d57f2b9705 @tomwidmer tomwidmer committed Jan 16, 2013
@@ -432,8 +432,8 @@ else if (prop instanceof ToOne) {
Object embeddedInstance =
createObjectFromEmbeddedNativeEntry(embedded.getAssociatedEntity(), embeddedEntry);
ea.setProperty(propKey, embeddedInstance);
- if (embedded.isBidirectional()) {
- Association inverseSide = embedded.getInverseSide();
+ Association inverseSide = embedded.getInverseSide();
+ if (embedded.isBidirectional() && inverseSide != null) {
// fix up the owner link
EntityAccess embeddedEa =
createEntityAccess(embedded.getAssociatedEntity(), embeddedInstance);
@@ -491,8 +491,24 @@ else if (tmp != null && !prop.getType().isInstance(tmp)) {
}
}
else if (prop instanceof EmbeddedCollection) {
- final Object embeddedInstances = getEntryValue(nativeEntry, propKey);
- loadEmbeddedCollection((EmbeddedCollection)prop, ea, embeddedInstances, propKey);
+ final Object embeddedInstances = getEntryValue(nativeEntry, propKey);
+ EmbeddedCollection embeddedCollection = (EmbeddedCollection) prop;
+ loadEmbeddedCollection(embeddedCollection, ea, embeddedInstances, propKey);
+ Association inverseSide = embeddedCollection.getInverseSide();
+ if (embeddedCollection.isBidirectional() && inverseSide != null) {
+ // fix up the inverse link
+ Object loadedInstances = ea.getProperty(embeddedCollection.getName());
+ if (loadedInstances instanceof Collection) {
+ Collection embeddedInstancesCollection = (Collection) loadedInstances;
+ for (Object embeddedInstance : embeddedInstancesCollection) {
+ if (embeddedInstance != null) {
+ EntityAccess embeddedEa =
+ createEntityAccess(embeddedCollection.getAssociatedEntity(), embeddedInstance);
+ embeddedEa.setProperty(inverseSide.getName(), obj);
+ }
+ }
+ }
+ }
}
else if (prop instanceof OneToMany) {
Association association = (Association) prop;
@@ -1142,10 +1158,16 @@ else if (persistentProperty instanceof Custom) {
}
}
else if (persistentProperty instanceof Association) {
- if (persistentProperty instanceof Embedded) {
+ Association inverseSide = ((Association) persistentProperty).getInverseSide();
+ if (inverseSide instanceof Embedded ||
+ inverseSide instanceof EmbeddedCollection) {
+ // these are the back links to the parents we might be embedded in, and don't need to be saved.
+ // they are recreated during a refresh.
+ }
+ else if (persistentProperty instanceof Embedded) {
Association toOne = (Association) persistentProperty;
- handleEmbeddedToOne(toOne,getPropertyKey(persistentProperty) , embeddedEntityAccess, embeddedEntry);
+ handleEmbeddedToOne(toOne, getPropertyKey(persistentProperty), embeddedEntityAccess, embeddedEntry);
}
else if (persistentProperty instanceof ToOne) {
Association toOne = (Association) persistentProperty;
@@ -49,12 +49,7 @@
import org.grails.datastore.mapping.model.MappingFactory;
import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.datastore.mapping.model.PersistentProperty;
-import org.grails.datastore.mapping.model.types.Association;
-import org.grails.datastore.mapping.model.types.Basic;
-import org.grails.datastore.mapping.model.types.EmbeddedCollection;
-import org.grails.datastore.mapping.model.types.ManyToMany;
-import org.grails.datastore.mapping.model.types.OneToOne;
-import org.grails.datastore.mapping.model.types.ToOne;
+import org.grails.datastore.mapping.model.types.*;
import org.grails.datastore.mapping.reflect.ClassPropertyFetcher;
import org.grails.datastore.mapping.reflect.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -197,54 +192,28 @@ public boolean isPersistentEntity(Class clazz) {
// and it is defined as persistent
if (embedded.contains(propertyName)) {
if (isCollectionType(currentPropType)) {
- Class relatedClassType = (Class) hasManyMap.get(propertyName);
- if (relatedClassType == null) {
- Class javaClass = entity.getJavaClass();
-
- Class genericClass = MappingUtils.getGenericTypeForProperty(javaClass, propertyName);
-
- if (genericClass != null) {
- relatedClassType = genericClass;
- }
- }
- if (relatedClassType != null) {
- if (propertyFactory.isSimpleType(relatedClassType)) {
- Basic basicCollection = propertyFactory.createBasicCollection(entity, context, descriptor);
- persistentProperties.add(basicCollection);
- }
- else {
- EmbeddedCollection association = propertyFactory.createEmbeddedCollection(
- entity, context, descriptor);
-
- persistentProperties.add(association);
- if (isPersistentEntity(relatedClassType)) {
- association.setAssociatedEntity( getOrCreateAssociatedEntity(entity, context, relatedClassType) );
- }
- else {
- PersistentEntity embeddedEntity = context.createEmbeddedEntity(relatedClassType);
- embeddedEntity.initialize();
- association.setAssociatedEntity(embeddedEntity);
- }
- }
+ final Association association = establishRelationshipForCollection(descriptor, entity, context, hasManyMap, mappedByMap, true);
+ if (association != null) {
+ persistentProperties.add(association);
}
}
else {
- ToOne association = propertyFactory.createEmbedded(entity, context, descriptor);
- PersistentEntity associatedEntity = getOrCreateEmbeddedEntity(entity, context, association.getType());
- association.setAssociatedEntity(associatedEntity);
- persistentProperties.add(association);
+ final ToOne association = establishDomainClassRelationship(entity, descriptor, context, hasOneMap, true);
+ if (association != null) {
+ persistentProperties.add(association);
+ }
}
}
else if (isCollectionType(currentPropType)) {
- final Association association = establishRelationshipForCollection(descriptor, entity, context, hasManyMap, mappedByMap);
+ final Association association = establishRelationshipForCollection(descriptor, entity, context, hasManyMap, mappedByMap, false);
if (association != null) {
configureOwningSide(association);
persistentProperties.add(association);
}
}
// otherwise if the type is a domain class establish relationship
else if (isPersistentEntity(currentPropType)) {
- final ToOne association = establishDomainClassRelationship(entity, descriptor, context, hasOneMap);
+ final ToOne association = establishDomainClassRelationship(entity, descriptor, context, hasOneMap, false);
if (association != null) {
configureOwningSide(association);
persistentProperties.add(association);
@@ -308,24 +277,49 @@ private Set establishRelationshipOwners(ClassPropertyFetcher cpf) {
return owners;
}
- private Association establishRelationshipForCollection(PropertyDescriptor property, PersistentEntity entity, MappingContext context, Map<String, Class> hasManyMap, Map mappedByMap) {
+ private Association establishRelationshipForCollection(PropertyDescriptor property, PersistentEntity entity, MappingContext context, Map<String, Class> hasManyMap, Map mappedByMap, boolean embedded) {
// is it a relationship
Class relatedClassType = hasManyMap.get(property.getName());
+ // try a bit harder for embedded collections (could make this the default, rendering 'hasMany' optional
+ // if generics are used)
+ if (relatedClassType == null && embedded) {
+ Class javaClass = entity.getJavaClass();
+
+ Class genericClass = MappingUtils.getGenericTypeForProperty(javaClass, property.getName());
+
+ if (genericClass != null) {
+ relatedClassType = genericClass;
+ }
+ }
+
if (relatedClassType == null) {
return propertyFactory.createBasicCollection(entity, context, property);
}
+ if (embedded) {
+ if (propertyFactory.isSimpleType(relatedClassType)) {
+ return propertyFactory.createBasicCollection(entity, context, property);
+ }
+ else if (!isPersistentEntity(relatedClassType)) {
+ // no point in setting up bidirectional link here, since target isn't an entity.
+ EmbeddedCollection association = propertyFactory.createEmbeddedCollection(entity, context, property);
+ PersistentEntity associatedEntity = getOrCreateEmbeddedEntity(entity, context, relatedClassType);
+ association.setAssociatedEntity(associatedEntity);
+ return association;
+ }
+ }
+ else if (!isPersistentEntity(relatedClassType)) {
+ // otherwise set it to not persistent as you can't persist
+ // relationships to non-domain classes
+ return propertyFactory.createBasicCollection(entity, context, property);
+ }
+
// set the referenced type in the property
ClassPropertyFetcher referencedCpf = ClassPropertyFetcher.forClass(relatedClassType);
String referencedPropertyName = null;
// if the related type is a domain class
// then figure out what kind of relationship it is
- if (!isPersistentEntity(relatedClassType)) {
- // otherwise set it to not persistent as you can't persist
- // relationships to non-domain classes
- return propertyFactory.createBasicCollection(entity, context, property);
- }
// check the relationship defined in the referenced type
// if it is also a Set/domain class etc.
@@ -427,8 +421,11 @@ else if (descriptors.size() > 1) {
final boolean isInverseSideEntity = isPersistentEntity(relatedClassPropertyType);
Association association = null;
boolean many = false;
- if (relatedClassPropertyType == null || isInverseSideEntity) {
- // uni-directional one-to-many
+ if (embedded) {
+ association = propertyFactory.createEmbeddedCollection(entity, context, property);
+ }
+ else if (relatedClassPropertyType == null || isInverseSideEntity) {
+ // uni or bi-directional one-to-many
association = propertyFactory.createOneToMany(entity, context, property);
}
else if (Collection.class.isAssignableFrom(relatedClassPropertyType) ||
@@ -454,6 +451,7 @@ else if (Collection.class.isAssignableFrom(relatedClassPropertyType) ||
if (association != null) {
association.setAssociatedEntity(associatedEntity);
if (referencedPropertyName != null) {
+ // bidirectional
association.setReferencedPropertyName(referencedPropertyName);
}
}
@@ -514,10 +512,20 @@ private PropertyDescriptor findProperty(List<PropertyDescriptor> descriptors, St
* @param property Establishes a relationship between this class and the domain class property
* @param context
* @param hasOneMap
+ * @param embedded
*/
- private ToOne establishDomainClassRelationship(PersistentEntity entity, PropertyDescriptor property, MappingContext context, Map hasOneMap) {
+ private ToOne establishDomainClassRelationship(PersistentEntity entity, PropertyDescriptor property, MappingContext context, Map hasOneMap, boolean embedded) {
ToOne association = null;
Class propType = property.getPropertyType();
+
+ if (embedded && !isPersistentEntity(propType)) {
+ // uni-directional to embedded non-entity
+ PersistentEntity associatedEntity = getOrCreateEmbeddedEntity(entity, context, propType);
+ association = propertyFactory.createEmbedded(entity, context, property);
+ association.setAssociatedEntity(associatedEntity);
+ return association;
+ }
+
ClassPropertyFetcher cpf = ClassPropertyFetcher.forClass(propType);
// establish relationship to type
@@ -580,18 +588,19 @@ else if (descriptors.length > 1) {
}
// establish relationship based on this type
- // uni-directional one-to-one
final boolean isAssociationEntity = isPersistentEntity(relatedClassPropertyType);
+ // one-to-one
if (relatedClassPropertyType == null || isAssociationEntity) {
- association = propertyFactory.createOneToOne(entity, context, property);
+ association = embedded ? propertyFactory.createEmbedded(entity, context, property) :
+ propertyFactory.createOneToOne(entity, context, property);
- if (hasOneMap.containsKey(property.getName())) {
+ if (hasOneMap.containsKey(property.getName()) && !embedded) {
association.setForeignKeyInChild(true);
}
}
// bi-directional many-to-one
- else if (Collection.class.isAssignableFrom(relatedClassPropertyType)||Map.class.isAssignableFrom(relatedClassPropertyType)) {
+ else if (!embedded && Collection.class.isAssignableFrom(relatedClassPropertyType)||Map.class.isAssignableFrom(relatedClassPropertyType)) {
association = propertyFactory.createManyToOne(entity, context, property);
}
@@ -5,8 +5,9 @@ import grails.persistence.Entity
class EmbeddedAssociationSpec extends GormDatastoreSpec {
- static {
- GormDatastoreSpec.TEST_CLASSES << Individual << Individual2 << Address << LongAddress
+ @Override
+ List getDomainClasses() {
+ return [Individual, Individual2, Address, LongAddress]
}
void "Test persistence of embedded entities"() {
@@ -62,6 +63,37 @@ class EmbeddedAssociationSpec extends GormDatastoreSpec {
}
+ void "Test persistence of embedded entities with links to parent"() {
+ given:"A domain with an embedded association"
+ def i = new Individual(name:"Bob", address: new Address(postCode:"30483"))
+ i.address.individual = i
+ i.save(flush:true)
+ session.clear()
+
+ when:"When domain is queried"
+ i = Individual.findByName("Bob")
+
+ then:"The embedded association is correctly loaded"
+ i != null
+ i.name == 'Bob'
+ i.address != null
+ i.address.postCode == '30483'
+ i.address.individual == i
+
+ when:"The embedded association is updated"
+ i.address = new Address(postCode: '28749')
+ i.save(flush:true)
+ session.clear()
+ i = Individual.get(i.id)
+
+ then:"The embedded association is correctly updated"
+ i != null
+ i.name == 'Bob'
+ i.address != null
+ i.address.postCode == '28749'
+ i.address.individual == i
+ }
+
void "Test persistence of embedded entity collections"() {
given:"An entity with an embedded entity collection"
def i = new Individual2(name:"Bob", address: new Address(postCode:"30483"))
@@ -100,6 +132,44 @@ class EmbeddedAssociationSpec extends GormDatastoreSpec {
}
+ void "Test persistence of embedded collections with links to parent"() {
+ given:"A domain with an embedded association"
+ def i = new Individual2(name:"Bob", address: new Address(postCode:"30483"))
+
+ when:"A collection is added via addTo"
+ [new Address(postCode: "12345"), new Address(postCode: "23456")].each { i.addToOtherAddresses(it) }
+
+ then:"Back-links are populated"
+ i.otherAddresses[0].individual2 == i
+ i.otherAddresses[1].individual2 == i
+
+ when:"Entity is saved and session is cleared"
+ i.save(flush:true)
+ session.clear()
+ i = Individual2.findByName("Bob")
+
+ then:"The object was correctly persisted"
+ i != null
+ i.name == 'Bob'
+ i.otherAddresses != null
+ i.otherAddresses.size() == 2
+ i.otherAddresses[0].individual2 == i
+ i.otherAddresses[1].individual2 == i
+
+ when:"The embedded association is updated"
+ i.otherAddresses = [new Address(postCode: '28749')]
+ i.save(flush:true)
+ session.clear()
+ i = Individual2.get(i.id)
+
+ then:"The embedded association is correctly updated"
+ i != null
+ i.name == 'Bob'
+ i.otherAddresses != null
+ i.otherAddresses.size() == 1
+ i.otherAddresses[0].individual2 == i
+ }
+
void "Test persistence of embedded sub-class entities"() {
given:"A domain with an embedded association"
def i = new Individual(name:"Oliver", address: new LongAddress(postCode:"30483", firstLine: "1 High Street",
@@ -218,6 +288,15 @@ class Individual2 {
class Address {
Long id
String postCode
+ Individual individual
+ Individual2 individual2
+
+ static belongsTo = [Individual, Individual2]
+
+ static constraints = {
+ individual nullable: true
+ individual2 nullable: true
+ }
}
@Entity

0 comments on commit 7bf24d0

Please sign in to comment.