@@ -70,6 +70,8 @@ public virtual class fflib_SObjectUnitOfWork
7070
7171 protected IDML m_dml ;
7272
73+ protected Boolean m_attemptToResolveOutOfOrderRelationships = false ;
74+
7375 /**
7476 * Interface describes work to be performed during the commitWork method
7577 **/
@@ -135,6 +137,22 @@ public virtual class fflib_SObjectUnitOfWork
135137 public virtual void onCommitWorkFinishing () {}
136138 public virtual void onCommitWorkFinished (Boolean wasSuccessful ) {}
137139
140+ /**
141+ * Calling this allows relationships class to track relationships that weren't resolved on insert and attempt
142+ * to set the relationship in a subsequent update. This is slightly less efficient than the default behaviour
143+ * of silently failing to set the lookup in this scenario.
144+ *
145+ * @return this unit of work
146+ */
147+ public fflib_SObjectUnitOfWork attemptToResolveOutOfOrderRelationships ()
148+ {
149+ m_attemptToResolveOutOfOrderRelationships = true ;
150+ for (Relationships relationships : m_relationships .values ()) {
151+ relationships .attemptToResolveOutOfOrderRelationships (true );
152+ }
153+ return this ;
154+ }
155+
138156 /**
139157 * Registers the type to be used for DML operations
140158 *
@@ -147,7 +165,8 @@ public virtual class fflib_SObjectUnitOfWork
147165 m_newListByType .put (sObjectType .getDescribe ().getName (), new List <SObject >());
148166 m_dirtyMapByType .put (sObjectType .getDescribe ().getName (), new Map <Id , SObject >());
149167 m_deletedMapByType .put (sObjectType .getDescribe ().getName (), new Map <Id , SObject >());
150- m_relationships .put (sObjectType .getDescribe ().getName (), new Relationships ());
168+ m_relationships .put (sObjectType .getDescribe ().getName (),
169+ new Relationships ().attemptToResolveOutOfOrderRelationships (m_attemptToResolveOutOfOrderRelationships ));
151170
152171 // give derived class opportunity to register the type
153172 onRegisterType (sObjectType );
@@ -360,6 +379,21 @@ public virtual class fflib_SObjectUnitOfWork
360379 m_relationships .get (sObjectType .getDescribe ().getName ()).resolve ();
361380 m_dml .dmlInsert (m_newListByType .get (sObjectType .getDescribe ().getName ()));
362381 }
382+
383+ // Resolve any unresolved relationships where parent was inserted after child, and so child lookup was not set
384+ if (m_attemptToResolveOutOfOrderRelationships )
385+ {
386+ for (Schema .SObjectType sObjectType : m_sObjectTypes )
387+ {
388+ Relationships relationships = m_relationships .get (sObjectType .getDescribe ().getName ());
389+ if (relationships .hasParentInsertedAfterChild ())
390+ {
391+ List <SObject > childrenToUpdate = relationships .resolveParentInsertedAfterChild ();
392+ m_dml .dmlUpdate (childrenToUpdate );
393+ }
394+ }
395+ }
396+
363397 // Update by type
364398 for (Schema .SObjectType sObjectType : m_sObjectTypes )
365399 m_dml .dmlUpdate (m_dirtyMapByType .get (sObjectType .getDescribe ().getName ()).values ());
@@ -405,6 +439,24 @@ public virtual class fflib_SObjectUnitOfWork
405439 private class Relationships
406440 {
407441 private List <IRelationship > m_relationships = new List <IRelationship >();
442+ private Boolean m_attemptToResolveOutOfOrderRelationships = false ;
443+ private List <RelationshipPermittingOutOfOrderInsert > m_parentInsertedAfterChildRelationships =
444+ new List <RelationshipPermittingOutOfOrderInsert >();
445+
446+ /**
447+ * Calling this allows relationships class to track relationships that weren't resolved on insert and attempt
448+ * to set the relationship in a subsequent update. This is slightly less efficient than the default behaviour
449+ * of silently failing to set the lookup in this scenario.
450+ *
451+ * @param attemptToResolve If true then will track relationships that weren't resolved on insert
452+ *
453+ * @return this object
454+ */
455+ public Relationships attemptToResolveOutOfOrderRelationships (Boolean attemptToResolve )
456+ {
457+ m_attemptToResolveOutOfOrderRelationships = attemptToResolve ;
458+ return this ;
459+ }
408460
409461 public void resolve ()
410462 {
@@ -413,18 +465,85 @@ public virtual class fflib_SObjectUnitOfWork
413465 {
414466 // relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id);
415467 relationship .resolve ();
468+
469+ // Check if parent is inserted after the child
470+ if (m_attemptToResolveOutOfOrderRelationships &&
471+ relationship instanceof RelationshipPermittingOutOfOrderInsert &&
472+ ! ((RelationshipPermittingOutOfOrderInsert ) relationship ).Resolved )
473+ {
474+ m_parentInsertedAfterChildRelationships .add ((RelationshipPermittingOutOfOrderInsert ) relationship );
475+ }
416476 }
477+ }
417478
479+ /**
480+ * @return true if there are unresolved relationships
481+ */
482+ public Boolean hasParentInsertedAfterChild ()
483+ {
484+ return ! m_parentInsertedAfterChildRelationships .isEmpty ();
485+ }
486+
487+ /**
488+ * Call this after all records in the UOW have been inserted to set the lookups on the children that were
489+ * inserted before the parent was inserted
490+ *
491+ * @throws UnitOfWorkException if the parent still does not have an ID - can occur if parent is not registered
492+ * @return The child records to update in order to set the lookups
493+ */
494+ public List <SObject > resolveParentInsertedAfterChild () {
495+ for (RelationshipPermittingOutOfOrderInsert relationship : m_parentInsertedAfterChildRelationships )
496+ {
497+ relationship .resolve ();
498+ if (! relationship .Resolved )
499+ {
500+ throw new UnitOfWorkException (' Error resolving relationship where parent is inserted after child.' +
501+ ' The parent has not been inserted. Is it registered with a unit of work?' );
502+ }
503+ }
504+ return getChildRecordsWithParentInsertedAfter ();
505+ }
506+
507+ /**
508+ * Call after calling resolveParentInsertedAfterChild()
509+ *
510+ * @return The child records to update in order to set the lookups
511+ */
512+ private List <SObject > getChildRecordsWithParentInsertedAfter ()
513+ {
514+ // Get rid of dupes
515+ Map <Id , SObject > recordsToUpdate = new Map <Id , SObject >();
516+ for (RelationshipPermittingOutOfOrderInsert relationship : m_parentInsertedAfterChildRelationships )
517+ {
518+ SObject childRecord = relationship .Record ;
519+ SObject recordToUpdate = recordsToUpdate .get (childRecord .Id );
520+ if (recordToUpdate == null )
521+ recordToUpdate = childRecord .getSObjectType ().newSObject (childRecord .Id );
522+ recordToUpdate .put (relationship .RelatedToField , childRecord .get (relationship .RelatedToField ));
523+ recordsToUpdate .put (recordToUpdate .Id , recordToUpdate );
524+ }
525+ return recordsToUpdate .values ();
418526 }
419527
420528 public void add (SObject record , Schema.sObjectField relatedToField , SObject relatedTo )
421529 {
422530 // Relationship to resolve
423- Relationship relationship = new Relationship ();
424- relationship .Record = record ;
425- relationship .RelatedToField = relatedToField ;
426- relationship .RelatedTo = relatedTo ;
427- m_relationships .add (relationship );
531+ if (! m_attemptToResolveOutOfOrderRelationships )
532+ {
533+ Relationship relationship = new Relationship ();
534+ relationship .Record = record ;
535+ relationship .RelatedToField = relatedToField ;
536+ relationship .RelatedTo = relatedTo ;
537+ m_relationships .add (relationship );
538+ }
539+ else
540+ {
541+ RelationshipPermittingOutOfOrderInsert relationship = new RelationshipPermittingOutOfOrderInsert ();
542+ relationship .Record = record ;
543+ relationship .RelatedToField = relatedToField ;
544+ relationship .RelatedTo = relatedTo ;
545+ m_relationships .add (relationship );
546+ }
428547 }
429548
430549 public void add (Messaging.SingleEmailMessage email , SObject relatedTo )
@@ -453,6 +572,34 @@ public virtual class fflib_SObjectUnitOfWork
453572 }
454573 }
455574
575+ private class RelationshipPermittingOutOfOrderInsert implements IRelationship {
576+ public SObject Record ;
577+ public Schema.sObjectField RelatedToField ;
578+ public SObject RelatedTo ;
579+ public Boolean Resolved = false ;
580+
581+ public void resolve ()
582+ {
583+ if (RelatedTo .Id == null ) {
584+ /*
585+ If relationship is between two records in same table then update is always required to set the lookup,
586+ so no warning is needed. Otherwise the caller may be able to be more efficient by reordering the order
587+ that the records are inserted, so alert the caller of this.
588+ */
589+ if (RelatedTo .getSObjectType () != Record .getSObjectType ()) {
590+ System .debug (System .LoggingLevel .WARN , ' Inefficient use of register relationship, related to ' +
591+ ' record should be first in dependency list to save an update; parent should be inserted ' +
592+ ' before child so child does not need an update. In unit of work initialization put ' +
593+ ' ' + RelatedTo .getSObjectType () + ' before ' + Record .getSObjectType ());
594+ }
595+ resolved = false ;
596+ } else {
597+ Record .put (RelatedToField , RelatedTo .Id );
598+ resolved = true ;
599+ }
600+ }
601+ }
602+
456603 private class EmailRelationship implements IRelationship
457604 {
458605 public Messaging.SingleEmailMessage email ;
0 commit comments