Strongly Typed Parent & Child-level Queries
Table of Contents:
One of the best thing about using strongly typed query builders is the type-checking that you get — for free — while using them. Type-checking not only prevents you, as a developer, from making a mistake — it also prevents admins from updating the API name for a field or from being able to delete it while it’s referenced within your Apex code. Even if there were no other use for the Schema.SObjectField
class, those two reasons alone would make it worth our while when deciding when to use these field-level tokens in our code. 1 Let’s look at how to take full advantage of those strongly typed field tokens in our existing query builder!
Despite the incredible utility of Schema.SObjectField
, there are two places where its utility falls a bit short of being fully usable across the possible spectrum of Salesforce object fields:
- parent-level fields can be referenced (which is great!) but they require a bit more work before they can be passed into queries
- lists of children fields aren’t supported
This is … a bit of a let-down, to say the least. For 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
So from a technical perspective, the hurdle we need to clear for parent-level field tokens is the result of Schema.DescribeFieldResult
not including a getSObjectType()
method (in other words, we can get describe tokens for parent-level fields, but there’s no dynamic reference to Account
if we were using the example from above). For child-level fields, we have access to the getChildRelationships()
method off of the parent’s Schema.DescribeSObjectResult
, but this returns a List<Schema.ChildRelationship>
that we then have to iterate through ourselves (awkward). Regardless — it’s eminently possible (and not that hard) to add parent/child-level query support to the Repository Pattern.
Update — as of Spring ‘23, Schema.DescribeFieldResult
does have a getSObjectType()
method on it! However this doesn’t quite work for our use-case, as complex Schema.SObjectField
references (like Opportunity.Account.Owner.Name
) will only have the top-level User
object reference, leaving us unable to fully recreate the hierarchy relationship name. Read on to see the full solve:
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) {
}
Running the test, we of course get a failure: System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Opportunity.Account
. Perfect! This is just the failure we need in order to proceed! This is also a chance to add in a non-obvious optimization: previously, the list of fields being queried for was being dynamically appended within Repository
any time a query is made — but the selection fields are “static”, in the sense that they don’t change. Since we’ll need to add to them while still having the base fields passed to each repository instance, this is a sensible time to move 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
}
We’ve moved one line of code and added eight more. That’s all that it takes for the test to pass. This is a good indication that the Repository
conforms to the Open-Closed Principle. Not shown here — but perhaps a fun exercise for the reader — would be how we could easily check the List<SObjectType> parentTypes
list prior to adding parent fields, to ensure nobody was ever trying to navigate more than 5 objects “upwards.”
Something else to note — one of the reasons that the Repository
class that I end up showing off employs the usage of a RepoFactory
is specifically to aid and abet with consolidating where things like formulating the SELECT
part of SOQL statements is done. The combination of the repository pattern with the factory pattern is a powerful one; this is in contrast to something like the Selector pattern, which typically espouses separate selector classes for each queryable concern. Said another way — Repository encapsulates CRUD for any given object, whereas Selector more specifically encapsulates only the R in CRUD — read access to an object. It’s too “thin” of an abstraction for my taste.
Here’s what the RepoFactory
looks like, if we move our example repository there:
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
It only took 8 lines of code to add parent-level support — let’s see what sort of changes are due to the Repository
in order to add subselect, or child queries. As always, we’ll start by writing a failing test:
// 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
}
So — not quite as easy to add in as our parent-level support, weighing in at a frightening twenty-three lines of code added. As is always the case, too, there’s plenty of potential for additional extension — for example, adding Query
support to our addChildFields
method (to allow for filtering on the subselect). That opens the door to things like LIMIT
ing the results, ordering them, and so on. It’s my hope that showing how easy it is to add this functionality will enable others to make use of Repository
as a truly one-stop-shop for any and all CRUD needs in your Salesforce codebase when it comes to:
- strongly typed queries (which in and of itself deserves calling out the positive benefits like developer intellisense, platform validation of field API names, and all that good stuff)
- easily setting up test data without having to setup and act on complex hierarchical record trees. Simply use the
RepoFactoryMock
class (and its@TestVisible
properties) to easily stub out all CRUD operations
Wrapping Up Parent/Child Query Support
I think I set a land-speed record for finishing a Joys Of Apex article while writing this, and I’d like to thank Joseph Mason for providing the inspiration that spurred me to do so. He first commented on You Need A Strongly Typed Query Builder, and then later was good enough to digitally meet up so that we could review some instances where adding in support for child relationship queries would make a big difference. I was glad to have the chance to quickly expand upon the existing functionality in the hopes that it will end up serving others well. In addition to the ever-present code examples hosted on the Apex Mocks Stress Test repo — which now includes the code examples shown here — the functionality shown off here is also hosted on the official DML/Repository repo.
Thanks, as always, for taking the time to read the Joys Of Apex — it means a lot to me, and hopefully it proves helpful for you! Till next time.
-
Of course, there is an exception to every rule and that exception - use of truly String-based queries - comes when creating packages that need to be able to dynamically reference SObjects and fields that may/may not exist in a given org where the package will be installed. In Apex Rollup, for example, there is a section of the code that behaves differently in the event that it's being run in a multi-currency org. Because orgs where multi-currency isn't enabled don't contain objects like CurrencyType, the only way to reference them is in a string-based query
↩ go back from whence you came!