Skip to content

Commit

Permalink
Merge pull request apex-enterprise-patterns#20 from OrtooApps/feature…
Browse files Browse the repository at this point in the history
…/cached-soql-executor

Feature/cached soql executor
  • Loading branch information
rob-baillie-ortoo committed Mar 7, 2022
2 parents 50aab44 + a2f3aa6 commit 9c5361c
Show file tree
Hide file tree
Showing 22 changed files with 813 additions and 214 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<PlatformCachePartition xmlns="http://soap.sforce.com/2006/04/metadata">
<description>The core platform cache partition for caching soql results</description>
<isDefaultPartition>true</isDefaultPartition>
<masterLabel>soql</masterLabel>
<platformCachePartitionTypes>
<allocatedCapacity>0</allocatedCapacity>
<allocatedPartnerCapacity>0</allocatedPartnerCapacity>
<allocatedPurchasedCapacity>0</allocatedPurchasedCapacity>
<allocatedTrialCapacity>0</allocatedTrialCapacity>
<cacheType>Session</cacheType>
</platformCachePartitionTypes>
<platformCachePartitionTypes>
<allocatedCapacity>3</allocatedCapacity>
<allocatedPartnerCapacity>3</allocatedPartnerCapacity>
<allocatedPurchasedCapacity>0</allocatedPurchasedCapacity>
<allocatedTrialCapacity>0</allocatedTrialCapacity>
<cacheType>Organization</cacheType>
</platformCachePartitionTypes>
</PlatformCachePartition>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public inherited sharing class FrameworkErrorCodes {

public final static String NON_EVALUATABLE_CRITERIA = 'CRI-00000';

public final static String CACHE_ACCESS_VIOLATION = 'CACHE-00000';


public final static String DML_ON_INACCESSIBLE_FIELDS = '0000000';
public final static String DML_INSERT_NOT_ALLOWED = '0000001';
public final static String DML_UPDATE_NOT_ALLOWED = '0000002';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Provides the ability to cache the results of particular SOQL statements in the Org Wide Platform Cache.
*
* Should only be used for queries that reference data that does not generally change.
*
* If used, it is recommended that triggers are added to those objects, or code added to the UI that updates the objects
* referenced in the SOQL that invalidate the cache.
*/
public inherited sharing class OrgCachedSoqlExecutor
{
public class OrgCacheAccessViolationException extends Exceptions.DeveloperException {} // this looks like a config exception, but actually the system should be built
// in such a way that it's never possible to get this exception

private final static String SOQL_PARTITION_NAME = 'soql';
private final static Integer CACHE_LIFESPAN_SECONDS = 43200; // TODO: soft setting / option

@testVisible
private final static String CAN_ACCESS_SOQL_CACHE_PERMISSION = 'ProcessesCanAccessSOQLCache';

private Boolean hasAccessToCache
{
get
{
if ( hasAccessToCache == null )
{
hasAccessToCache = PermissionsService.hasPermission( CAN_ACCESS_SOQL_CACHE_PERMISSION );
}
return hasAccessToCache;
}
set;
}

/**
* Perform the given query, first checking if the Org Platform Cache Partition contains results for that SOQL.
* If so, the cached versions are returned.
* If not, the query is executed against the database and the result cached.
* If, for any reason, a cache read or write cannot be performed, the method will continue without an exception.
* Errors can be seen in the System.debug log.
*
* @param String The SOQL to return the results for
* @return List<Sobject> The records that match
*/
public List<Sobject> query( String soql )
{
Contract.requires( soql != null, 'query called with a null soql' );

String key = generateKey( soql );
List<Sobject> returnValues = null;

try
{
if ( hasAccessToCache )
{
returnValues = (List<Sobject>)Cache.Org.get( key );
}
else
{
System.debug( LoggingLevel.INFO, 'Opportunity to use Org Platform Cache skipped since user does not have required permission (custom permission: ' + CAN_ACCESS_SOQL_CACHE_PERMISSION + ')' );
}
}
catch ( cache.Org.OrgCacheException e )
{
System.debug( LoggingLevel.ERROR, 'Attempt to read from the Org Platform Cache failed for the SOQL: ' + soql );
System.debug( LoggingLevel.ERROR, e );
}

if ( returnValues == null )
{
if ( hasAccessToCache )
{
System.debug( LoggingLevel.INFO, 'Org Platform Cache miss when running the SOQL: ' + soql );
}

returnValues = Database.query( soql );

try
{
if ( hasAccessToCache )
{
Cache.Org.put( key, returnValues, CACHE_LIFESPAN_SECONDS, Cache.Visibility.NAMESPACE, false ); // immutable results
}
}
catch ( Exception e )
{
System.debug( LoggingLevel.ERROR, 'Attempt to write into the Org Platform Cache failed for the SOQL: ' + soql );
System.debug( LoggingLevel.ERROR, e );
}
}

return returnValues;
}

/**
* Clears the cached results for the given SOQL that are held in the Org Platform Cache Partition
*
* @param String The SOQL to clear the cache for
*/
public void clearCacheFor( String soql )
{
Contract.requires( soql != null, 'clearCacheFor called with a null soql' );

if ( ! hasAccessToCache )
{
throw new OrgCacheAccessViolationException( Label.ortoo_core_soql_cache_access_violation )
.setErrorCode( FrameworkErrorCodes.CACHE_ACCESS_VIOLATION )
.addContext( 'method', 'clearCacheFor' )
.addContext( 'soql', soql );
}
Cache.Org.remove( generateKey( soql ) );
}

/**
* Clears the cached results for all cached SOQL
*/
public void clearAllCache()
{
if ( ! hasAccessToCache )
{
throw new OrgCacheAccessViolationException( Label.ortoo_core_soql_cache_access_violation )
.setErrorCode( FrameworkErrorCodes.CACHE_ACCESS_VIOLATION )
.addContext( 'method', 'clearAllCache' );
}

String fullSoqlPartitionName = Cache.OrgPartition.createFullyQualifiedPartition( PackageUtils.NAMESPACE_PREFIX, SOQL_PARTITION_NAME );
for ( String thisKey : Cache.Org.getKeys() )
{
String qualifiedKey = qualifiedKey( thisKey );
if ( Cache.Org.contains( qualifiedKey ) )
{
Cache.Org.remove( qualifiedKey );
}
}
}

private String generateKey( String soql )
{
String subkey = EncodingUtil.convertToHex( Crypto.generateDigest( 'SHA1', Blob.valueOf( soql ) ) );
return qualifiedKey( subkey );
}

private String qualifiedKey( String subkey )
{
return Cache.OrgPartition.createFullyQualifiedKey( PackageUtils.NAMESPACE_PREFIX, SOQL_PARTITION_NAME, subkey );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,44 @@ public inherited sharing class ortoo_DynamicSobjectSelector extends ortoo_Sobjec
*/
public List<Sobject> selectByCriteria( ortoo_Criteria criteria )
{
Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' );
return selectByCriteria( criteria, false );
}

/**
* Retrieve the records that match the passed criteria, optionally via the Org Level SOQL Cache
*
* @param ortoo_Criteria The criteria that should be used to derive the records to return
* @param Boolean Should the query go via the Org Level SOQL Cache?
* @return List<Sobject> The result of the Selection
*/
public List<Sobject> selectByCriteria( ortoo_Criteria criteria, Boolean cached )
{
Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' );
Contract.requires( cached != null, 'selectByCriteria called with a null cached' );
Contract.assert( sobjectType != null, 'selectByCriteria called when sobjectType has not been set' );

return Database.query( generateSoqlByCriteria( criteria ) );
String soql = generateSoqlByCriteria( criteria );
if ( cached )
{
return ((OrgCachedSoqlExecutor)Application.APP_LOGIC.newInstance( OrgCachedSoqlExecutor.class ))
.query( soql );
}
return Database.query( soql );
}

/**
* Request that the cached results for the given criteria be cleared.
* Assumes that the fields are set up correctly.
*
* @param ortoo_Criteria The criteria that should be used to derive the SOQL results to clear
*/
public ortoo_DynamicSobjectSelector clearCacheFor( ortoo_Criteria criteria )
{
Contract.requires( criteria != null, 'clearCacheFor called with a null criteria' );

((OrgCachedSoqlExecutor)Application.APP_LOGIC.newInstance( OrgCachedSoqlExecutor.class ))
.clearCacheFor( generateSoqlByCriteria( criteria ) );
return this;
}

/**
Expand Down
Loading

0 comments on commit 9c5361c

Please sign in to comment.