Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support reflective lookups and setting lookups when parent is inserted before child #195

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
199 changes: 191 additions & 8 deletions fflib/src/classes/fflib_SObjectUnitOfWork.cls
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@
public virtual class fflib_SObjectUnitOfWork
implements fflib_ISObjectUnitOfWork
{

/*
* Unit of work has two ways of resolving registered relationships that require an update to resolve (e.g. parent
* and child are same sobject type, or the parent is inserted after the child):
*
* AttemptResolveOutOfOrder - Update child to set the relationship (e.g. insert parent, insert child, update child)
* IgnoreOutOfOrder (default behaviour) - Do not set the relationship (e.g. leave lookup null)
*/
public enum UnresolvedRelationshipBehavior { AttemptResolveOutOfOrder, IgnoreOutOfOrder }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdrbc is it ok if you change these enum values to camel case?


private static final UnresolvedRelationshipBehavior DEFAULT_UNRESOLVED_RELATIONSHIP_BEHAVIOR =
UnresolvedRelationshipBehavior.IgnoreOutOfOrder;

protected List<Schema.SObjectType> m_sObjectTypes = new List<Schema.SObjectType>();

protected Map<String, List<SObject>> m_newListByType = new Map<String, List<SObject>>();
Expand All @@ -75,6 +88,8 @@ public virtual class fflib_SObjectUnitOfWork

protected IDML m_dml;

protected final UnresolvedRelationshipBehavior m_unresolvedRelationshipBehaviour;

/**
* Interface describes work to be performed during the commitWork method
**/
Expand Down Expand Up @@ -120,8 +135,46 @@ public virtual class fflib_SObjectUnitOfWork
this(sObjectTypes,new SimpleDML());
}

/**
* Constructs a new UnitOfWork to support work against the given object list
*
* @param sObjectTypes A list of objects given in dependency order (least dependent first)
* @param unresolvedRelationshipsBehaviour If AttemptOutOfOrderRelationships and a relationship is registered
* where a parent is inserted after a child then will update the child
* post-insert to set the relationship. If IgnoreOutOfOrder then
* relationship will not be set.
*/
public fflib_SObjectUnitOfWork(List<Schema.SObjectType> sObjectTypes,
UnresolvedRelationshipBehavior unresolvedRelationshipBehavior) {
this(sObjectTypes, new SimpleDML(), unresolvedRelationshipBehavior);
}

/**
* Constructs a new UnitOfWork to support work against the given object list
*
* @param sObjectTypes A list of objects given in dependency order (least dependent first)
* @param dml Modify the database via this class
*/
public fflib_SObjectUnitOfWork(List<Schema.SObjectType> sObjectTypes, IDML dml)
{
this(sObjectTypes, dml, DEFAULT_UNRESOLVED_RELATIONSHIP_BEHAVIOR);
}

/**
* Constructs a new UnitOfWork to support work against the given object list
*
* @param sObjectTypes A list of objects given in dependency order (least dependent first)
* @param dml Modify the database via this class
* @param unresolvedRelationshipsBehaviour If AttemptOutOfOrderRelationships and a relationship is registered
* where a parent is inserted after a child then will update the child
* post-insert to set the relationship. If IgnoreOutOfOrder then relationship
* will not be set.
*/
public fflib_SObjectUnitOfWork(List<Schema.SObjectType> sObjectTypes, IDML dml,
UnresolvedRelationshipBehavior unresolvedRelationshipBehavior)
{
m_unresolvedRelationshipBehaviour = unresolvedRelationshipBehavior;

m_sObjectTypes = sObjectTypes.clone();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the point of this list cloning?


for (Schema.SObjectType sObjectType : m_sObjectTypes)
Expand All @@ -130,7 +183,8 @@ public virtual class fflib_SObjectUnitOfWork
handleRegisterType(sObjectType);
}

m_relationships.put(Messaging.SingleEmailMessage.class.getName(), new Relationships());
m_relationships.put(Messaging.SingleEmailMessage.class.getName(),
new Relationships(unresolvedRelationshipBehavior));

m_dml = dml;
}
Expand Down Expand Up @@ -171,7 +225,7 @@ public virtual class fflib_SObjectUnitOfWork
m_newListByType.put(sObjectName, new List<SObject>());
m_dirtyMapByType.put(sObjectName, new Map<Id, SObject>());
m_deletedMapByType.put(sObjectName, new Map<Id, SObject>());
m_relationships.put(sObjectName, new Relationships());
m_relationships.put(sObjectName, new Relationships(m_unresolvedRelationshipBehaviour));

m_publishBeforeListByType.put(sObjectName, new List<SObject>());
m_publishAfterSuccessListByType.put(sObjectName, new List<SObject>());
Expand Down Expand Up @@ -353,7 +407,7 @@ public virtual class fflib_SObjectUnitOfWork
**/
public void registerUpsert(SObject record)
{
if (record.Id == null)
if (record.Id == null)
{
registerNew(record, null, null);
}
Expand Down Expand Up @@ -574,6 +628,20 @@ public virtual class fflib_SObjectUnitOfWork
m_relationships.get(sObjectType.getDescribe().getName()).resolve();
m_dml.dmlInsert(m_newListByType.get(sObjectType.getDescribe().getName()));
}

// Resolve any unresolved relationships where parent was inserted after child, and so child lookup was not set
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdrbc this code looks like it could all be indented by one level.

if (m_unresolvedRelationshipBehaviour == UnresolvedRelationshipBehavior.AttemptResolveOutOfOrder)
{
for(Schema.SObjectType sObjectType : m_sObjectTypes)
{
Relationships relationships = m_relationships.get(sObjectType.getDescribe().getName());
if (relationships.hasParentInsertedAfterChild())
{
List<SObject> childrenToUpdate = relationships.resolveParentInsertedAfterChild();
m_dml.dmlUpdate(childrenToUpdate);
}
}
}
}

private void updateDmlByType()
Expand Down Expand Up @@ -666,6 +734,23 @@ public virtual class fflib_SObjectUnitOfWork
private class Relationships
{
private List<IRelationship> m_relationships = new List<IRelationship>();
private List<RelationshipPermittingOutOfOrderInsert> m_parentInsertedAfterChildRelationships =
new List<RelationshipPermittingOutOfOrderInsert>();
private final UnresolvedRelationshipBehavior m_unresolvedRelationshipBehaviour;

/**
* Unit of work has two ways of resolving registered relationships that require an update to resolve (e.g.
* parent and child are same sobject type, or the parent is inserted after the child):
*
* AttemptResolveOutOfOrder - Update child to set the relationship (e.g. insert parent, insert child, update
* child)
* IgnoreOutOfOrder (default behaviour) - Do not set the relationship (e.g. leave lookup null)
*
* @param unresolvedRelationshipBehaviour The behaviour to use when encountering unresolved relationships
*/
public Relationships(UnresolvedRelationshipBehavior unresolvedRelationshipBehaviour) {
m_unresolvedRelationshipBehaviour = unresolvedRelationshipBehaviour;
}

public void resolve()
{
Expand All @@ -674,18 +759,84 @@ public virtual class fflib_SObjectUnitOfWork
{
//relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id);
relationship.resolve();

// Check if parent is inserted after the child
if (m_unresolvedRelationshipBehaviour == UnresolvedRelationshipBehavior.AttemptResolveOutOfOrder &&
!((RelationshipPermittingOutOfOrderInsert) relationship).Resolved)
{
m_parentInsertedAfterChildRelationships.add((RelationshipPermittingOutOfOrderInsert) relationship);
}
}
}

/**
* @return true if there are unresolved relationships
*/
public Boolean hasParentInsertedAfterChild()
{
return !m_parentInsertedAfterChildRelationships.isEmpty();
}

/**
* Call this after all records in the UOW have been inserted to set the lookups on the children that were
* inserted before the parent was inserted
*
* @throws UnitOfWorkException if the parent still does not have an ID - can occur if parent is not registered
* @return The child records to update in order to set the lookups
*/
public List<SObject> resolveParentInsertedAfterChild() {
for (RelationshipPermittingOutOfOrderInsert relationship : m_parentInsertedAfterChildRelationships)
{
relationship.resolve();
if (!relationship.Resolved)
{
throw new UnitOfWorkException('Error resolving relationship where parent is inserted after child.' +
' The parent has not been inserted. Is it registered with a unit of work?');
}
}
return getChildRecordsWithParentInsertedAfter();
}

/**
* Call after calling resolveParentInsertedAfterChild()
*
* @return The child records to update in order to set the lookups
*/
private List<SObject> getChildRecordsWithParentInsertedAfter()
{
// Get rid of dupes
Map<Id, SObject> recordsToUpdate = new Map<Id, SObject>();
for (RelationshipPermittingOutOfOrderInsert relationship : m_parentInsertedAfterChildRelationships)
{
SObject childRecord = relationship.Record;
SObject recordToUpdate = recordsToUpdate.get(childRecord.Id);
if (recordToUpdate == null)
recordToUpdate = childRecord.getSObjectType().newSObject(childRecord.Id);
recordToUpdate.put(relationship.RelatedToField, childRecord.get(relationship.RelatedToField));
recordsToUpdate.put(recordToUpdate.Id, recordToUpdate);
}
return recordsToUpdate.values();
}

public void add(SObject record, Schema.sObjectField relatedToField, SObject relatedTo)
{
// Relationship to resolve
Relationship relationship = new Relationship();
relationship.Record = record;
relationship.RelatedToField = relatedToField;
relationship.RelatedTo = relatedTo;
m_relationships.add(relationship);
if (m_unresolvedRelationshipBehaviour == UnresolvedRelationshipBehavior.IgnoreOutOfOrder)
{
Relationship relationship = new Relationship();
relationship.Record = record;
relationship.RelatedToField = relatedToField;
relationship.RelatedTo = relatedTo;
m_relationships.add(relationship);
}
else
{
RelationshipPermittingOutOfOrderInsert relationship = new RelationshipPermittingOutOfOrderInsert();
relationship.Record = record;
relationship.RelatedToField = relatedToField;
relationship.RelatedTo = relatedTo;
m_relationships.add(relationship);
}
}

public void add(Messaging.SingleEmailMessage email, SObject relatedTo)
Expand Down Expand Up @@ -714,6 +865,38 @@ public virtual class fflib_SObjectUnitOfWork
}
}

/**
* Similar to Relationship, but has a Resolved property that is set to false when relationship is not resolved
* because RelatedTo does not have an ID and/or resolve() has not been called.
*/
private class RelationshipPermittingOutOfOrderInsert implements IRelationship {
public SObject Record;
public Schema.sObjectField RelatedToField;
public SObject RelatedTo;
public Boolean Resolved = false;

public void resolve()
{
if (RelatedTo.Id == null) {
/*
If relationship is between two records in same table then update is always required to set the lookup,
so no warning is needed. Otherwise the caller may be able to be more efficient by reordering the order
that the records are inserted, so alert the caller of this.
*/
if (RelatedTo.getSObjectType() != Record.getSObjectType()) {
System.debug(System.LoggingLevel.WARN, 'Inefficient use of register relationship, related to ' +
'record should be first in dependency list to save an update; parent should be inserted ' +
'before child so child does not need an update. In unit of work initialization put ' +
'' + RelatedTo.getSObjectType() + ' before ' + Record.getSObjectType());
}
Resolved = false;
} else {
Record.put(RelatedToField, RelatedTo.Id);
Resolved = true;
}
}
}

private class EmailRelationship implements IRelationship
{
public Messaging.SingleEmailMessage email;
Expand Down
71 changes: 71 additions & 0 deletions fflib/src/classes/fflib_SObjectUnitOfWorkTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,77 @@ private with sharing class fflib_SObjectUnitOfWorkTest
Opportunity.SObjectType,
OpportunityLineItem.SObjectType };

@isTest
private static void testDoNotSupportOutOfOrderRelationships() {
// Insert contacts before accounts
List<Schema.SObjectType> dependencyOrder =
new Schema.SObjectType[] {
Contact.SObjectType,
Account.SObjectType
};

fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(dependencyOrder);
List<Contact> contacts = new List<Contact>();
for(Integer i=0; i<10; i++)
{
Account acc = new Account(Name = 'Account ' + i);
uow.registerNew(new List<SObject>{acc});
Contact cont = new Contact(LastName='Contact ' + i);
contacts.add(cont);
uow.registerNew(cont, Contact.AccountId, acc);
}

uow.commitWork();

// Assert that the lookups were not set (default behaviour)
contacts = [
SELECT AccountId
FROM Contact
WHERE Id IN :contacts
];
for (Contact cont : contacts) {
System.assertEquals(null, cont.AccountId);
}
}
@isTest
private static void testSupportOutOfOrderRelationships() {
// Insert contacts before accounts
List<Schema.SObjectType> dependencyOrder =
new Schema.SObjectType[] {
Contact.SObjectType,
Account.SObjectType
};

fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(dependencyOrder,
fflib_SObjectUnitOfWork.UnresolvedRelationshipBehavior.AttemptResolveOutOfOrder);
List<Account> accounts = new List<Account>();
List<Contact> contacts = new List<Contact>();
for(Integer i=0; i<10; i++)
{
Account acc = new Account(Name = 'Account ' + i);
uow.registerNew(new List<SObject>{acc});
accounts.add(acc);
Contact cont = new Contact(LastName='Contact ' + i);
contacts.add(cont);
uow.registerNew(cont, Contact.AccountId, acc);
}

uow.commitWork();

// Assert that the lookups were set
Map<Id, Contact> contactMap = new Map<Id, Contact> ([
SELECT AccountId
FROM Contact
WHERE Id IN :contacts
]);

for (Integer i = 0; i < 10; i++) {
Contact cont = contacts[i];
Account acc = accounts[i];
System.assertEquals(acc.Id, contactMap.get(cont.Id).AccountId);
}
}

@isTest
private static void testUnitOfWorkEmail()
{
Expand Down