Skip to content

Commit 19edc33

Browse files
author
John Rogers
committed
support reflective lookups and setting lookups when parent is inserted before child
1 parent 43d2bc8 commit 19edc33

File tree

2 files changed

+224
-6
lines changed

2 files changed

+224
-6
lines changed

fflib/src/classes/fflib_SObjectUnitOfWork.cls

Lines changed: 153 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

fflib/src/classes/fflib_SObjectUnitOfWorkTest.cls

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,77 @@ private with sharing class fflib_SObjectUnitOfWorkTest
3535
Opportunity.SObjectType,
3636
OpportunityLineItem.SObjectType };
3737

38+
@isTest
39+
private static void testDoNotSupportOutOfOrderRelationships() {
40+
// Insert contacts before accounts
41+
List<Schema.SObjectType> dependencyOrder =
42+
new Schema.SObjectType[] {
43+
Contact.SObjectType,
44+
Account.SObjectType
45+
};
46+
47+
fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(dependencyOrder);
48+
List<Contact> contacts = new List<Contact>();
49+
for(Integer i=0; i<10; i++)
50+
{
51+
Account acc = new Account(Name = 'Account ' + i);
52+
uow.registerNew(new List<SObject>{acc});
53+
Contact cont = new Contact(LastName='Contact ' + i);
54+
contacts.add(cont);
55+
uow.registerNew(cont, Contact.AccountId, acc);
56+
}
57+
58+
uow.commitWork();
59+
60+
// Assert that the lookups were not set (default behaviour)
61+
contacts = [
62+
SELECT AccountId
63+
FROM Contact
64+
WHERE Id IN :contacts
65+
];
66+
for (Contact cont : contacts) {
67+
System.assertEquals(null, cont.AccountId);
68+
}
69+
}
70+
@isTest
71+
private static void testSupportOutOfOrderRelationships() {
72+
// Insert contacts before accounts
73+
List<Schema.SObjectType> dependencyOrder =
74+
new Schema.SObjectType[] {
75+
Contact.SObjectType,
76+
Account.SObjectType
77+
};
78+
79+
fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(dependencyOrder)
80+
.attemptToResolveOutOfOrderRelationships();
81+
List<Account> accounts = new List<Account>();
82+
List<Contact> contacts = new List<Contact>();
83+
for(Integer i=0; i<10; i++)
84+
{
85+
Account acc = new Account(Name = 'Account ' + i);
86+
uow.registerNew(new List<SObject>{acc});
87+
accounts.add(acc);
88+
Contact cont = new Contact(LastName='Contact ' + i);
89+
contacts.add(cont);
90+
uow.registerNew(cont, Contact.AccountId, acc);
91+
}
92+
93+
uow.commitWork();
94+
95+
// Assert that the lookups were set
96+
Map<Id, Contact> contactMap = new Map<Id, Contact> ([
97+
SELECT AccountId
98+
FROM Contact
99+
WHERE Id IN :contacts
100+
]);
101+
102+
for (Integer i = 0; i < 10; i++) {
103+
Contact cont = contacts[i];
104+
Account acc = accounts[i];
105+
System.assertEquals(acc.Id, contactMap.get(cont.Id).AccountId);
106+
}
107+
}
108+
38109
@isTest
39110
private static void testUnitOfWorkEmail()
40111
{

0 commit comments

Comments
 (0)