Saturday, 14 January 2017

Salesforce Platform Cache - Expect the Unexpected

Platform Cache - Expect the Unexpected

Cache

Introduction

When the Salesforce Platform Cache appeared, this allowed a lot of home made caching code to be retired. Up until the the only data that was cached was custom settings. (This is in terms of not requiring a trip to the database, there are obviously all sorts of caches at play in the Salesforce platform - reports involving large amounts of data often succeed on the second or third try as some of the data has made it’s way closer to the front end). While custom settings speed up the retrieval of data and save SOQL calls, it was a pretty basic solution, requiring DML to push information into the ‘cache’, and the type of data that could be stored was pretty basic, so caching and rehydrating a complex object meant you had to handle the transformation yourself. 

In this post I won’t be going into the details of getting started with the cache, as the Trailhead module does an excellent job of this already.

Seek, and you may not find it

A key feature of the platform cache is that just because you put something in the cache, it doesn’t mean that it will still be there at a later date, thus you have to be prepared to deal with cache misses. Thus if I store an opportunity keyed by it’s id in a Blue Peter style partition I created earlier:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
Opportunity opp=[select id, Name, CloseDate from Opportunity where id='0060Y000003AWjG'];
oppsPart.put('0060Y000003AWjG', opp);

I can try to retrieve it in a different request:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
Opportunity oppFromCache=(Opportunity) oppsPart.get('0060Y000003AWjG');
System.debug('Opp = ' + oppFromCache);

and all things being equal I'll get the following debug output:

12:27:52:158 USER_DEBUG [3]|DEBUG|Opp = Opportunity:{Name=Edge Installation,
Id=0060Y000003AWjGQAW, CloseDate=2014-11-02 00:00:00}

If the opportunity has been booted from the cache for any reason (expired, an evil co-worker removes it) then I'll receive a null rather than the opportunity, and can fetch it from the database.

You have to know what you are asking for

Based on the above, if you are doing a lot of work with opportunities matching a particular criteria, it’s tempting to try to use the cache as a database replacement, storing all of them in there keyed by id:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> opps=[select id, Name, CloseDate from Opportunity];
for (Opportunity opp : opps)
{
	oppsPart.put(opp.id, opp);
}

I can then retrieve these by iterating all of the keys in the partition:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> opps=[select id, Name, CloseDate from Opportunity];
for (Opportunity opp : opps)
{
	oppsPart.put(opp.id, opp);
}

which gives me the following output:

12:42:40:176 USER_DEBUG [6]|DEBUG|Opp = Opportunity:{Name=Edge SLA,
Id=0060Y000003AWjHQAW, CloseDate=2014-11-02 00:00:00}
12:42:40:180 USER_DEBUG [6]|DEBUG|Opp = Opportunity:{Name=United Oil
Installations, Id=0060Y000003AWjFQAW, CloseDate=2014-11-02 00:00:00}
...
12:42:40:261 USER_DEBUG [6]|DEBUG|Opp = Opportunity:{Name=Burlington Textiles
Weaving Plant Generator, Id=0060Y000003AWjOQAW, CloseDate=2014-11-02 00:00:00}

At first glance this looks fine, but there is a huge issue with this approach - I have no idea whether this is the full collection of opportunities that I cached. Entries might have been evicted, even while I was iterating the opportunities I’d queried if the partition filled up, or an evil co-worker may remove a couple and then add them back later so that I have no idea why they weren’t processed.

Unlike executing  SOQL query, all I can be sure of is that I’ve retrieved all of the opportunities that remain in the cache, which may bear very little relation to the contents of the database. Clearly another approach is required. 

Cache collections as a single entry

In this scenario, rather than storing each object as an entry the solution is to store the entire collection as a single entry:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> opps=[select id, Name, CloseDate from Opportunity];
oppsPart.put('all', opps);

retrieving it in a separate request as follows:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> oppsFromCache=(List<Opportunity>) oppsPart.get('all');
for (Opportunity opp : oppsFromCache)
{
	System.debug('Opp = ' + opp);
}

In this case, if my opportunities have been evicted from the cache I’ll receive a null response and can re-query them from the database. Of course this doesn’t stop an evil co-worker from overwriting the ‘all' entry in the cache with a different collection of opportunities - if this continues to be a problem then a session cache partition, which is tied to a specific user, is probably a better option.

More Information