Add getParentFieldById methods to fflib_SObjects2 (#73)

This method can fetch deeply related data which is distributed over multiple related domains
This commit is contained in:
William Velzeboer
2026-01-20 10:42:08 +00:00
committed by GitHub
parent 82e84354d5
commit 5c14faa02e
4 changed files with 271 additions and 7 deletions
@@ -104,6 +104,129 @@ public virtual class fflib_SObjects2
return (result.size() == 0) ? null : result.get(0);
}
/**
* Gets the field value from the last given grandparent domain by the record Id
*
* @param valueField The field use return as value for the map
* @param domainsByRelationshipField A Map of domains and the in hierarchical order from first parent to grandparent
*
* @return Returns a map with KEY: The Ids of the records in this domain,
* to the VALUE: The value of the given valueField of the last grandparent record listed in the relatedDomains,
* null if no grandparent exists.
*
* @example
* ICases casesDomain = Cases.newInstance(caseRecords);
* IContacts contactsDomain = Contacts.newInstance(contactRecords);
* IAccount accountDomain = Accounts.newInstance(accountRecords);
*
* // A map with key - Case.Id to value - Account.Id
* Map<Id, String> accountNameByCaseId =
* casesDomain.getParentStringFieldById(
* Schema.Account.Name,
* new Map<Schema.SObjectField, fflib_SObjects2>
* {
* Schema.Case.ContactId => contactDomain,
* Schema.Contact.AccountId => accountDomain
* });
*/
public virtual Map<Id, String> getParentStringFieldById(
Schema.SObjectField valueField,
Map<Schema.SObjectField, fflib_SObjects2> domainsByRelationshipField)
{
Map<Id, Id> parentIdByChildId = getParentIdById(domainsByRelationshipField);
List<fflib_SObjects2> domains = domainsByRelationshipField.values();
fflib_SObjects2 lastGrandParentDomain = domains.get(domains.size() - 1);
return fflib_ArrayUtils.replaceValue(
parentIdByChildId,
lastGrandParentDomain.getStringFieldByIdField(
valueField,
lastGrandParentDomain.getSObjectType().getDescribe().fields.getMap().get('Id')));
}
/**
* Gets the field value from the last given grandparent domain by the record Id
*
* @param valueField The field use return as value for the map
* @param domainsByRelationshipField A Map of domains and the in hierarchical order from first parent to grandparent
*
* @return Returns a map with KEY: The Ids of the records in this domain,
* to the VALUE: The value of the given valueField of the last grandparent record listed in the relatedDomains,
* null if no grandparent exists.
*
* @example
* ICases casesDomain = Cases.newInstance(caseRecords);
* IContacts contactsDomain = Contacts.newInstance(contactRecords);
* IAccount accountDomain = Accounts.newInstance(accountRecords);
*
* // A map with key - Case.Id to value - Account.Id
* Map<Id, Object> accountNameByCaseId =
* casesDomain.getParentDecimalFieldById(
* Schema.Account.Name,
* new Map<Schema.SObjectField, fflib_SObjects2>
* {
* Schema.Case.ContactId => contactDomain,
* Schema.Contact.AccountId => accountDomain
* });
*/
public virtual Map<Id, Decimal> getParentDecimalFieldById(
Schema.SObjectField valueField,
Map<Schema.SObjectField, fflib_SObjects2> domainsByRelationshipField)
{
Map<Id, Id> parentIdByChildId = getParentIdById(domainsByRelationshipField);
List<fflib_SObjects2> domains = domainsByRelationshipField.values();
fflib_SObjects2 lastGrandParentDomain = domains.get(domains.size() - 1);
return fflib_ArrayUtils.replaceValue(
parentIdByChildId,
lastGrandParentDomain.getDecimalById(
valueField,
lastGrandParentDomain.getSObjectType().getDescribe().fields.getMap().get('Id')));
}
/**
* Gets the field value from the last given grandparent domain by the record Id
*
* @param valueField The field use return as value for the map
* @param domainsByRelationshipField A Map of domains and the in hierarchical order from first parent to grandparent
*
* @return Returns a map with KEY: The Ids of the records in this domain,
* to the VALUE: The value of the given valueField of the last grandparent record listed in the relatedDomains,
* null if no grandparent exists.
*
* @example
* ICases casesDomain = Cases.newInstance(caseRecords);
* IContacts contactsDomain = Contacts.newInstance(contactRecords);
* IAccount accountDomain = Accounts.newInstance(accountRecords);
*
* // A map with key - Case.Id to value - Account.Id
* Map<Id, Object> accountNameByCaseId =
* casesDomain.getParentBooleanFieldById(
* Schema.Account.Name,
* new Map<Schema.SObjectField, fflib_SObjects2>
* {
* Schema.Case.ContactId => contactDomain,
* Schema.Contact.AccountId => accountDomain
* });
*/
public virtual Map<Id, Boolean> getParentBooleanFieldById(
Schema.SObjectField valueField,
Map<Schema.SObjectField, fflib_SObjects2> domainsByRelationshipField)
{
Map<Id, Id> parentIdByChildId = getParentIdById(domainsByRelationshipField);
List<fflib_SObjects2> domains = domainsByRelationshipField.values();
fflib_SObjects2 lastGrandParentDomain = domains.get(domains.size() - 1);
return fflib_ArrayUtils.replaceValue(
parentIdByChildId,
lastGrandParentDomain.getBooleanFieldByIdField(
valueField,
lastGrandParentDomain.getSObjectType().getDescribe().fields.getMap().get('Id')));
}
/**
* Creates a map between the record Ids of this domain to the last given grandparent domain.
*
@@ -129,11 +252,14 @@ public virtual class fflib_SObjects2
*/
public virtual Map<Id, Id> getParentIdById(Map<Schema.SObjectField, fflib_SObjects2> domainsByRelationshipField)
{
// Takes the highest related domain from the domainsByRelationshipField
// Clone the domain map as we are about to modify it within this method
Map<SObjectField, fflib_SObjects2> domains = domainsByRelationshipField.clone();
// Takes the highest related domain from the domains
Schema.SObjectField relationshipField =
new List<Schema.SObjectField>(domainsByRelationshipField.keySet())
new List<Schema.SObjectField>(domains.keySet())
.get(0);
fflib_SObjects2 relatedDomain = domainsByRelationshipField.remove(relationshipField);
fflib_SObjects2 relatedDomain = domains.remove(relationshipField);
// Extract the Id values of the related SObjectType
Map<Id, Id> result = getIdFieldByIdField(
@@ -141,10 +267,10 @@ public virtual class fflib_SObjects2
getSObjectType().getDescribe().fields.getMap().get('Id'));
// Check if we need to resolve another parent relation, if not return the results
if (domainsByRelationshipField.isEmpty()) return result;
if (domains.isEmpty()) return result;
// Fetch the Ids of the related SObjectType
Map<Id, Id> parentIdByChildId = relatedDomain.getParentIdById(domainsByRelationshipField);
Map<Id, Id> parentIdByChildId = relatedDomain.getParentIdById(domains);
// Replace the maps value (ParentId) with the grand parent Id
return fflib_ArrayUtils.replaceValue(result, parentIdByChildId);
@@ -207,7 +333,7 @@ public virtual class fflib_SObjects2
*/
public virtual Map<Id, Set<Id>> getChildIdsById(Map<Schema.SObjectField, fflib_SObjects2> domainsByRelationshipField)
{
// Takes the highest related domain from the domainsByRelationshipField
// Takes the highest related domain from the domains
Schema.SObjectField relationshipField =
new List<Schema.SObjectField>(domainsByRelationshipField.keySet())
.get(0);
@@ -552,6 +678,34 @@ public virtual class fflib_SObjects2
return result;
}
/**
* Get a map with the values of two fields.
* Key fields containing null values are omitted
*
* @param valueField The field to use as the Value of the Map
* @param keyField The Id field to use as the Key of the map
*
* @return Returns a map with the values of two fields
*
* @example
* Accounts accounts = Accounts.newInstance(records);
* Map<Id, Object> accountNameById = accounts.getFieldValueByIdField(Account.Name, Account.Id);
*/
@TestVisible
protected virtual Map<Id, Object> getFieldValueByIdField(SObjectField valueField, SObjectField keyField) {
Map<Id, Object> result = new Map<Id, Object>();
for (SObject record : getRecords())
{
if (record.get(keyField) == null) continue;
result.put(
(Id) record.get(keyField),
record.get(valueField)
);
}
return result;
}
/**
* @return Returns only the first SObject contained in the domain.
*/
@@ -373,6 +373,36 @@ public class fflib_ArrayUtils
return result;
}
/**
* Takes two maps and replaces the value of the source with the value of the replacement,
* where the source value matches the replacement key.
* The value will become null if the source value does not exists as key in replacement map
*
* @param source The map to replace its value
* @param replacement The map to use its value for replacement
*
* @return Returns a Map<{Source} Id, {Replacement} Id>
*/
public static Map<Id, Object> replaceValue(Map<Id, Id> source, Map<Id, Object> replacement)
{
Map<Id, Object> result = new Map<Id, Object>();
for (Id keyId : source.keySet())
{
Id sourceValue = source.get(keyId);
if (replacement.containsKey(sourceValue))
{
result.put(keyId, replacement.get(sourceValue));
}
else
{
// There is no replacement value for this keyId
result.put(keyId, null);
}
}
return result;
}
/**
* Takes two maps and replaces the value of the source with the value of the replacement,
* where the source value matches the replacement key.
@@ -257,6 +257,83 @@ private class fflib_SObjects2Test
System.Assert.isNull(result.get(caseIdC), 'Case Id B should have no related Account Id');
}
@IsTest
static void itShouldReturnTheRelatedParentStringById()
{
// GIVEN a case related to contact related to Account ,
// a case record related to contact but not to Account,
// and a case record related to nothing.
// All encapsulated in domains.
Id caseIdA = fflib_IDGenerator.generate(Schema.Case.SObjectType);
Id caseIdB = fflib_IDGenerator.generate(Schema.Case.SObjectType);
Id caseIdC = fflib_IDGenerator.generate(Schema.Case.SObjectType);
Id contactIdA = fflib_IDGenerator.generate(Schema.Contact.SObjectType);
Id contactIdB = fflib_IDGenerator.generate(Schema.Contact.SObjectType);
Id accountId = fflib_IDGenerator.generate(Schema.Account.SObjectType);
Account accountRecord = new Account(Id = accountId, Name = 'Account Name', AnnualRevenue = 100.1);
Contact contactRecordA = new Contact(Id = contactIdA, AccountId = accountId, DoNotCall = true);
Contact contactRecordB = new Contact(Id = contactIdB);
Case caseRecordA = new Case(Id = caseIdA, ContactId = contactIdA);
Case caseRecordB = new Case(Id = caseIdB, ContactId = contactIdB);
Case caseRecordC = new Case(Id = caseIdC);
DomainAccounts accountDomain = new DomainAccounts(new List<Account> {accountRecord});
DomainContacts contactDomain = new DomainContacts(new List<Contact> {contactRecordA, contactRecordB});
DomainCases caseDomain = new DomainCases(new List<Case> {caseRecordA, caseRecordB, caseRecordC});
// WHEN we request the relation between Case and Account
System.Test.startTest();
Map<Id, String> stringResult = caseDomain.getParentStringFieldById(
Schema.Account.Name,
new Map<Schema.SObjectField, fflib_SObjects2>
{
Schema.Case.ContactId => contactDomain,
Schema.Contact.AccountId => accountDomain
}
);
Map<Id, Decimal> decimalResult = caseDomain.getParentDecimalFieldById(
Schema.Account.AnnualRevenue,
new Map<Schema.SObjectField, fflib_SObjects2>
{
Schema.Case.ContactId => contactDomain,
Schema.Contact.AccountId => accountDomain
}
);
Map<Id, Boolean> booleanResult = caseDomain.getParentBooleanFieldById(
Schema.Contact.DoNotCall,
new Map<Schema.SObjectField, fflib_SObjects2>
{
Schema.Case.ContactId => contactDomain
}
);
System.Test.stopTest();
// THEN it should return the AccountId By CaseId
System.Assert.areEqual(3, stringResult.size(), 'Expected one related account to the case');
System.Assert.isTrue(stringResult.containsKey(caseIdA));
System.Assert.isTrue(stringResult.containsKey(caseIdB));
System.Assert.isTrue(stringResult.containsKey(caseIdC));
System.Assert.areEqual('Account Name', stringResult.get(caseIdA), 'Account Name should be related to the Case Id A');
System.Assert.isNull(stringResult.get(caseIdB), 'Case Id B should have no related Account');
System.Assert.isNull(stringResult.get(caseIdC), 'Case Id B should have no related Account');
System.Assert.areEqual(3, decimalResult.size(), 'Expected one related account to the case');
System.Assert.isTrue(decimalResult.containsKey(caseIdA));
System.Assert.isTrue(decimalResult.containsKey(caseIdB));
System.Assert.isTrue(decimalResult.containsKey(caseIdC));
System.Assert.areEqual(100.1, decimalResult.get(caseIdA), 'Account Name should be related to the Case Id A');
System.Assert.isNull(decimalResult.get(caseIdB), 'Case Id B should have no related Account');
System.Assert.isNull(decimalResult.get(caseIdC), 'Case Id B should have no related Account');
System.Assert.areEqual(3, booleanResult.size(), 'Expected one related account to the case');
System.Assert.isTrue(booleanResult.containsKey(caseIdA));
System.Assert.isTrue(booleanResult.containsKey(caseIdB));
System.Assert.isTrue(booleanResult.containsKey(caseIdC));
System.Assert.isTrue(booleanResult.get(caseIdA), 'Contact DoNotCall should be related to the Case Id A');
System.Assert.isNull(booleanResult.get(caseIdB), 'Case Id B should have no related Contact');
System.Assert.isNull(booleanResult.get(caseIdC), 'Case Id B should have no related Contact');
}
/** Unit test for the deprecated method **/
@IsTest
static void itShouldReturnTheRelatedParentDomainId_deprecated()