SOQL Cache
Apex Classes: SOQLCache.cls
and SOQLCache_Test.cls
.
The lib cache main class necessary to create cached selectors.
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual(Profile.Name, 'System Administrator')
.toObject();
Methods
The following are methods for using SOQLCache
:
with(SObjectField field)
with(SObjectField field1, SObjectField field2)
with(SObjectField field1, SObjectField field2, SObjectField field3)
with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4)
with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5)
with(List<SObjectField> fields)
with(String fields)
CACHE STORAGE
cacheInApexTransaction
Signature
Cacheable cacheInApexTransaction();
Example
public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
public static SOQL_ProfileCache query() {
return new SOQL_ProfileCache();
}
private SOQL_ProfileCache() {
super(Profile.SObjectType);
cacheInApexTransaction(); // <=== Cache in Apex Transaction
}
public override SOQL.Queryable initialQuery() {
return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
}
}
cacheInOrgCache
Signature
Cacheable cacheInOrgCache();
Example
public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
public static SOQL_ProfileCache query() {
return new SOQL_ProfileCache();
}
private SOQL_ProfileCache() {
super(Profile.SObjectType);
cacheInOrgCache(); // <=== Cache in Org Cache
}
public override SOQL.Queryable initialQuery() {
return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
}
}
cacheInSessionCache
Signature
Cacheable cacheInSessionCache();
Example
public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
public static SOQL_ProfileCache query() {
return new SOQL_ProfileCache();
}
private SOQL_ProfileCache() {
super(Profile.SObjectType);
cacheInSessionCache(); // <=== Cache in Session Cache
}
public override SOQL.Queryable initialQuery() {
return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
}
}
CACHE EXPIRATION
maxHoursWithoutRefresh
Default: 48 hours
All cached records have an additional field called cachedDate
. To avoid using outdated records, you can add maxHoursWithoutRefresh
to your query. This will check how old the cached record is and, if it’s too old, execute a query to update the record in the cache.
Signature
Cacheable maxHoursWithoutRefresh(Integer hours)
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual(Profile.Name, 'System Administrator')
.maxHoursWithoutRefresh(12)
.toObject();
INITIAL QUERY
The initial query allows for the bulk population of records in the cache (if it is empty), ensuring that every subsequent query in the cached selector will use the cached records.
For instance:
public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
public static SOQL_ProfileCache query() {
return new SOQL_ProfileCache();
}
private SOQL_ProfileCache() {
super(Profile.SObjectType);
cacheInOrgCache();
with(Profile.Id, Profile.Name, Profile.UserType)
}
public override SOQL.Queryable initialQuery() { // <=== Initial query
return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
}
public SOQL_ProfileCache byName(String name) {
whereEqual(Profile.Name, name);
return this;
}
}
When the cache is empty, the initialQuery
will be executed to populate the data in the cache. This allows SOQL_ProfileCache.query().byName('System Administrator').toObject();
to retrieve the profile from the already cached records, instead of fetching records individually.
initialQuery
Signature
SOQL.Queryable initialQuery()
Example
public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
public static SOQL_ProfileCache query() {
return new SOQL_ProfileCache();
}
private SOQL_ProfileCache() {
super(Profile.SObjectType);
cacheInOrgCache();
with(Profile.Id, Profile.Name, Profile.UserType)
}
public override SOQL.Queryable initialQuery() { // <=== Initial query
return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
}
}
SELECT
All selected fields are going to be cached.
with field1 - field5
Signature
Cacheable with(SObjectField field)
Cacheable with(SObjectField field1, SObjectField field2);
Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3);
Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual(Profile.Name, 'System Administrator')
.toObject();
SOQL.of(Profile.SObjectType)
.with(Profile.Id)
.with(Profile.Name)
.whereEqual(Profile.Name, 'System Administrator')
.toObject();
with fields
Use for more than 5 fields.
Signature
Cacheable with(List<SObjectField> fields)
Example
SOQLCache.of(Profile.SObjectType)
.with(new List<SObjectField>{ Profile.Id, Profile.Name, Profile.UserType })
.whereEqual(Profile.Name, 'System Administrator')
.toObject();
with string fields
NOTE! With String Apex does not create reference to field. Use SObjectField
whenever it possible. Method below should be only use for dynamic queries.
Signature
Cacheable with(String fields)
Example
SOQLCache.of(Profile.SObjectType)
.with('Id, Name, UserType')
.whereEqual(Profile.Name, 'System Administrator')
.toObject();
WHERE
A cached query must include one condition. The filter must use a cached field (defined in cachedFields()
) and should be based on Id
, Name
, DeveloperName
, or another unique field.
A query requires a single condition, and that condition must filter by a unique field.
To ensure that cached records are aligned with the database, a single condition is required. A query without a condition cannot guarantee that the number of records in the cache matches the database.
For example, let’s assume a developer makes the query: SELECT Id, Name FROM Profile
. Cached records will be returned, but they may differ from the records in the database.
The filter field should be unique. Consistency issues can arise when the field is not unique. For instance, the query:
SELECT Id, Name FROM Profile WHERE UserType = 'Standard'
may return some records, but the number of records in the cache may differ from those in the database.
Using a unique field ensures that if a record is not found in the cache, the SOQL library can look it up in the database.
Example
Cached Records:
Id | Name | UserType |
---|---|---|
00e3V000000Nme3QAC | System Administrator | Standard |
00e3V000000NmeAQAS | Standard Platform User | Standard |
00e3V000000NmeHQAS | Customer Community Plus User | PowerCustomerSuccess |
Database Records:
Id | Name | UserType |
---|---|---|
00e3V000000Nme3QAC | System Administrator | Standard |
00e3V000000NmeAQAS | Standard Platform User | Standard |
00e3V000000NmeZQAS | Read Only | Standard |
00e3V000000NmeYQAS | Solution Manager | Standard |
00e3V000000NmeHQAS | Customer Community Plus User | PowerCustomerSuccess |
Let’s assume a developer executes the query:
SELECT Id, Name, UserType FROM Profile WHERE UserType = 'Standard'
.
Since records exist in the cache, 2 records will be returned, which is incorrect. The database contains 4 records with UserType = 'Standard'
.
To avoid such scenarios, filtering by a unique field is required.
Sometimes, certain limitations can ensure that code functions in a deterministic and expected way. From our perspective, it is better to have limitations that make the code free from bugs and prevent unintended misuse.
whereEqual
Signature
Cacheable whereEqual(SObjectField field, Object value);
Cacheable whereEqual(String field, Object value);
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual(Profile.Name, 'System Administrator')
.toObject();
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual('Name', 'System Administrator')
.toObject();
FIELD-LEVEL SECURITY
stripInaccessible
The Security.stripInaccessible
method is the only one that works with cached records. Unlike WITH USER_MODE
, which works only with SOQL, Security.stripInaccessible
can remove inaccessible fields even from cached records.
Signature
Cacheable stripInaccessible()
Cacheable stripInaccessible(AccessType accessType)
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual('Name', 'System Administrator')
.stripInaccessible()
.toObject();
MOCKING
mockId
Developers can mock either the query or the cached result:
SOQLCache.setMock('queryId', record);
mocks cached results.SOQL.setMock('queryId', record);
mocks the query when cached records are not found.
We generally recommend using SOQLCache.setMock('queryId', record);
to ensure that records from the cache are not returned, which could otherwise lead to test instability.
Signature
Cacheable mockId(String queryIdentifier)
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual('Name', 'System Administrator')
.mockId('MyQuery')
.toObject();
// In Unit Test
SOQLCache.setMock('MyQuery', new Profile(Name = 'Mocked System Adminstrator'));
// or
SOQL.setMock('MyQuery', new Profile(Name = 'Mocked System Adminstrator'));
record mock
Signature
Cacheable setMock(String mockId, SObject record)
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual('Name', 'System Administrator')
.mockId('MyQuery')
.toObject();
// In Unit Test
SOQLCache.setMock('MyQuery', new Profile(Name = 'Mocked System Adminstrator'));
DEBUGGING
preview
Signature
Cacheable preview()
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.whereEqual('Name', 'System Administrator')
.preview()
.toObject();
Query preview will be available in debug logs:
============ Query Preview ============
SELECT Id, Name, UserType
FROM Profile
WHERE Name = :v1
=======================================
============ Query Binding ============
{
"v1" : "System Administrator"
}
=======================================
PREDEFINIED
byId
Signature
Cacheable byId(Id recordId)
Cacheable byId(SObject record)
Example
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.byId('00e3V000000Nme3QAC')
.toObject();
Profile profile = [SELECT Id FROM Profile WHERE Name = 'System Administrator'];
SOQLCache.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.byId(profile)
.toObject();
RESULT
toId
Id toId()
Example
new User (
// ...
ProfileId = SOQLCache.of(Profile.SObjectType).whereEqual('Name', 'System Administrator').toId()
);
doExist
Signature
Boolean doExist()
Example
Boolean isAdminProfileExists = SOQLCache.of(Profile.SObjectType)
.whereEqual('Name', 'System Administrator')
.doExist();
toValueOf
Extract field value from query result. The field must be in cached fields.
Signature
Object toValueOf(SObjectField fieldToExtract)
Example
String systemAdminUserType = (String) SOQLCache.of(Profile.SObjectType).byId('00e3V000000Nme3QAC').toValueOf(Profile.UserType);
toObject
When the list of records contains more than one entry, the error List has more than 1 row for assignment to SObject
will occur.
When there are no records to assign, the error List has no rows for assignment to SObject
will NOT occur. This is automatically handled by the framework, and a null
value will be returned instead.
Signature
Sbject toObject()
Example
Profile systemAdministratorProfile = (Profile) SOQLCache.of(Profile.SObjectType)
.whereEqual(Profile.Name, 'System Administrator')
.toObject();