Utilizing strongly typed query builders comes with a significant advantage: the inherent type-checking feature. This not only safeguards developers from potential errors but also prevents administrators from modifying the API name of a field or deleting it while it is referenced within Apex code. Even without considering other applications of the Schema.SObjectField class, these reasons alone justify the use of field-level tokens in our code. Let’s explore how to maximize the benefits of these strongly typed field tokens in our existing query builder.
However, despite the remarkable usefulness of Schema.SObjectField, there are two scenarios where its effectiveness falls slightly short of being universally applicable across all Salesforce object fields:
- Parent-level fields can be referenced, which is advantageous but requires additional steps before they can be incorporated into queries.
- Lists of children fields are not supported.
This limitation can be somewhat disappointing. Regarding parent-level fields:
// this is supported, but it only prints "Name" // NOT "Account.Name", which is what we would need // when building a query dynamically with this token Opportunity.Account.Name.getDescribe().getName();
And the equivalent child-level code:
Account.Opportunities.getDescribe() // not supported new Account().Opportunities.getSObjectType(); // supported! ... but not as useful
From a technical standpoint, addressing the challenge for parent-level field tokens stems from the absence of a getSObjectType() method in Schema.DescribeFieldResult. In other words, although we can obtain describe tokens for parent-level fields, there’s no dynamic reference to, for instance, “Account” if we were following the example mentioned earlier. On the other hand, for child-level fields, we can utilize the getChildRelationships() method from the parent’s Schema.DescribeSObjectResult. However, this returns a List<Schema.ChildRelationship> that requires manual iteration, which can be somewhat cumbersome. Nevertheless, it is entirely feasible (and not overly complex) to incorporate support for parent/child-level queries into the Repository Pattern.
Update: As of Spring ’23, Schema.DescribeFieldResult does include a getSObjectType() method. However, for our specific use case, complex Schema.SObjectField references (such as Opportunity.Account.Owner.Name) will only have the top-level User object reference. This leaves us unable to fully reconstruct the hierarchy relationship name. Continue reading to explore the complete resolution:
Adding Parent-Level Field Support
Since we are practicing Test Driven Development, let’s start with a failing test:
@IsTest private class RepositoryTests { @IsTest static void parentFieldsAreSupported() { IRepository repo = new OpportunityRepo(); // more on why the first arg // is a list in a second repo.addParentFields(new List<SObjectType>{ Account.SObjectType }, new List<SObjectField>{ Opportunity.Account.Id }); Account acc = new Account(Name = 'parent'); insert acc; insert new Opportunity(AccountId = acc.Id, StageName = 'testing', CloseDate = System.today(), Name = 'Child'); Opportunity retrievedOpp = (Opportunity) repo.getAll()[0]; System.assertNotEquals(null, retrievedOpp.Account.Id); } private class OpportunityRepo extends Repository { public OpportunityRepo() { super(Opportunity.SObjectType, new List<SObjectField>{ Opportunity.Id }, new RepoFactoryMock()); } } }
Note that we’ll need to stub out the implementation in order to be able to save and run the test:
public interface IRepository extends IDML { List<SObject> get(Query query); List<SObject> get(List<Query> queries); List<SObject> getAll(); // adds the method to the interface - we want the first arg // to be a list because you can go up multiple parents void addParentFields(List<SObjectType> parentType, List<SObjectField> parentFields); } // and then in Repository.cls: public void addParentFields(List<SObjectType> parentTypes, List<SObjectField> parentFields) { }
Executing the test results in an expected failure: System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Opportunity.Account. This failure serves as the necessary prompt to proceed with the task at hand. It also presents an opportunity to introduce a less obvious optimization. Previously, the list of queried fields was dynamically appended within the Repository each time a query was executed. However, the selection fields are essentially “static” since they do not change. Given that we will need to augment them while still retaining the base fields passed to each repository instance, this marks a logical moment to transfer the initial creation of the base selection fields to the constructor.
public virtual without sharing class Repository implements IRepository { // ... private final Set<String> selectFields; public Repository(Schema.SObjectType repoType, List<Schema.SObjectField> queryFields, RepoFactory repoFactory) { this(repoFactory); this.queryFields = queryFields; this.repoType = repoType; // this method already existed - it was just being executed // any time a query was made, before this.selectFields = this.addSelectFields(); } public void addParentFields(List<SObjectType> parentTypes, List<SObjectField> parentFields) { String parentBase = ''; for (SObjectType parentType : parentTypes) { parentBase += parentType.getDescribe().getName() + '.'; } for (SObjectField parentField : parentFields) { this.selectFields.add(parentBase + parentField.getDescribe().getName()); } } // ... etc }
By relocating a single line of code and incorporating eight additional lines, the test successfully passes. This demonstrates that the Repository adheres to the Open-Closed Principle. Although not depicted here, an engaging exercise for the reader could involve implementing a check on the List<SObjectType> parentTypes list before adding parent fields. This precaution would ensure that attempts to navigate more than five objects “upwards” are appropriately restricted.
Another noteworthy point is that the showcased Repository class utilizes a RepoFactory for a specific reason—to streamline tasks such as formulating the SELECT part of SOQL statements. The synergy between the repository pattern and the factory pattern proves potent. This approach differs from patterns like the Selector pattern, which typically advocates for separate selector classes for each queryable concern. In simpler terms, the Repository encapsulates CRUD operations for any given object, whereas the Selector specifically encapsulates only the “R” in CRUD—read access to an object. The Selector pattern is considered too “thin” of an abstraction for my preference.
If we move our example repository to the RepoFactory, here’s what the RepoFactory would resemble:
public virtual class RepoFactory { public virtual IAggregateRepository getOppRepo() { List<SObjectField> queryFields = new List<SObjectField>{ Opportunity.IsWon, Opportunity.StageName // etc ... }; IAggregateRepository oppRepo = new AggregateRepository(Opportunity.SObjectType, queryFields, this); oppRepo.addParentFields( new List<SObjectType>{ Account.SObjectType }, new List<SObjectField>{ Opportunity.Account.Id } ); return oppRepo; } // etc ... }
In this way, when the times comes to add new fields, you can do so in one place; this also is the “magic” behind being able to easily stub out query results (which I’ll talk more about after covering child queries)!
Adding Support For Subselects, Or Child Queries
Adding support for parent-level operations required just 8 lines of code. Now, let’s explore the adjustments needed for the Repository to accommodate subselect or child queries. As customary, we’ll commence by creating a test that intentionally fails:
// in RepositoryTests.cls @IsTest static void childFieldsAreSupported() { // luckily, outside of directly testing the repository // we rarely need to insert hierarchies of data like this Account acc = new Account(Name = 'parent'); insert acc; Contact con = new Contact(AccountId = acc.Id, LastName = 'Child field'); insert con; IRepository repo = new AccountRepo(); repo.addChildFields(Contact.SObjectType, new List<SObjectField>{ Contact.LastName }); acc = (Account) repo.getAll()[0]; // the test fails on the below line with: // "System.ListException: List index out of bounds: 0" System.assertEquals(con.LastName, acc.Contacts[0].LastName); } private class AccountRepo extends Repository { public AccountRepo() { super(Account.SObjectType, new List<SObjectField>{ Account.Id }, new RepoFactoryMock()); } }
I can’t stress enough the comment I’ve left there at the top of this test method — this is an extremely unusual situation, in the sense that it’s rare for me to actually be inserting data within tests because the Repository pattern allows — everywhere that repositories are used — easy access to stubbing out the return values needed in any given context. With that off my chest, let’s move on to the implementation:
public interface IRepository extends IDML { List<SObject> get(Query query); List<SObject> get(List<Query> queries); List<SObject> getAll(); void addParentFields(List<SObjectType> parentTypes, List<SObjectField> parentFields); // adding the method signature to the interface void addChildFields(SObjectType childType, List<SObjectField> childFields); } public virtual without sharing class Repository implements IRepository { // ... other instance properties private final Set<String> selectFields; private final Map<SObjectType, String> childToRelationshipNames; public Repository(Schema.SObjectType repoType, List<Schema.SObjectField> queryFields, RepoFactory repoFactory) { this(repoFactory); this.queryFields = queryFields; this.repoType = repoType; this.selectFields = this.addSelectFields(); this.childToRelationshipNames = this.getChildRelationshipNames(repoType); } // showing the private method called by the constructor first // for clarity: private Map<SObjectType, String> getChildRelationshipNames(Schema.SObjectType repoType) { Map<SObjectType, String> localChildToRelationshipNames = new Map<SObjectType, String>(); for (Schema.ChildRelationship childRelationship : repoType.getDescribe().getChildRelationships()) { localChildToRelationshipNames.put(childRelationship.getChildSObject(), childRelationship.getRelationshipName()); } return localChildToRelationshipNames; } // and then the implementation public void addChildFields(SObjectType childType, List<SObjectField> childFields) { if (this.childToRelationshipNames.containsKey(childType)) { String baseSubselect = '(SELECT {0} FROM {1})'; Set<String> childFieldNames = new Set<String>{ 'Id' }; for (SObjectField childField : childFields) { childFieldNames.add(childField.getDescribe().getName()); } this.selectFields.add( String.format( baseSubselect, new List<String>{ String.join(new List<String>(childFieldNames), ','), this.childToRelationshipNames.get(childType) } ) ); } } // ... etc, rest of the class }
Adding support for subselect or child queries is not as straightforward as the parent-level support; it involves incorporating a somewhat daunting twenty-three lines of additional code. As always, there’s ample room for further extension. For instance, enhancing Query support in our addChildFields method to enable filtering on the subselect opens up possibilities such as limiting results, applying order, and more. I aim to demonstrate that incorporating this functionality is straightforward, allowing others to leverage the Repository as a comprehensive solution for all CRUD requirements in their Salesforce codebase. This includes:
- Strongly typed queries, which offer benefits like developer IntelliSense and platform validation of field API names.
- Simplifying the setup of test data without the need for intricate hierarchical record tree arrangements. Users can employ the RepoFactoryMock class (along with its @TestVisible properties) to effortlessly stub out all CRUD operations.
Concluding Implementation of Parent/Child Query Support
Completing the Joys Of Apex article at a remarkable pace, I want to express my gratitude to Joseph Mason for inspiring and motivating me. His initial comment on “You Need A Strongly Typed Query Builder” and our subsequent digital meeting provided valuable insights on instances where incorporating support for child relationship queries could have a significant impact. I appreciate the opportunity to swiftly enhance the existing functionality, hoping it proves beneficial to others. The code examples, along with the showcased functionality, are available not only on the Apex Mocks Stress Test repository, which already houses numerous code examples, but also on the official DML/Repository repository.