1

I'm working on reducing data storage space and increasing the amount of automation for an integration with an external system. The relevant part of that integration for this question is a sync of product pricing information. This external system gives us about 120k rows for product pricing (~400 partners that resell our ~300 products). Each row contains 1 price for 1 product for 1 partner.

This product pricing information is effectively static, so I thought that the platform cache would be a good way to allow me to store this information and allow me to (immediately) delete the individual product pricing records (from Salesforce, not the external system)1.

A few iterations later, and I'm consolidating these 120k product pricing records into ~400. One per partner, which contain a serialized map of product Id to a price (in a longTextArea field). These ~400 (SObject) records act as the backing to my cache, and I've created a class that implements the CacheBuilder interface so I don't need to worry about cache misses. Also worthy of note is that the sum of this consolidated data is currently ~4MB.

The thing I'm concerned about is that when using a CacheBuilder, it appears that we can only retrieve a single key from the cache at a time and every miss is currently causing 1 query to be run. I don't think it's outside the realm of possibility for us to run into the 101 SOQL query limit, so I'd like to pre-warm the platform cache (i.e. put records I know I'm about to use into the cache before calling Cache.Session.get(MyCacheBuilder.class, 'my key');)

I fire up the trusty dev console, and execute something like the following:

CachePartition cp = Cache.Session.getPartition('MyPartitionName');
cp.put('myKey', 'abc123');

System.debug(cp.get(MyCacheBuilder.class, 'myKey'));

To my surprise, the debug says that there is no cached value for 'myKey', and I've used a query to boot. Further inspection using cp.getKeys() shows me that I have 2 keys in the cache, 'myKey' and 'MyCacheBuilder_B_myKey'.

So using a CacheBuilder prepends data to the key, and that's something Salesforce handles transparently. The issue is that we don't have a put(CacheBuilder, key, value) method, and the key for put(key, value) doesn't allow underscores, which is really stymying my attempt to pre-warm the cache.

Is there a way to populate the cache (without using get(CacheBuilder.class, key) so that I can fetch values using get(CacheBuilder.class, key)?

1: Yes, I'm aware that a Platform Event is probably a better choice here, given that I'm immediately deleting the record after some light processing. I'm trying to keep things relatively consistent (let's call it a soft requirement), and using a PE for just that one piece is unpalatable to my boss.

1 Answer 1

0

While it isn't exactly what I'm looking for, I did think of an acceptable workaround as I was writing the question.

My CacheBuilder implementation right now follows the example given in the documentation, and contains a SOQL query. Something like

public class MyCacheBuilder implements Cache.CacheBuilder{
    public Object doLoad(String identifier){
        Map<String, Decimal> result = new Map<String, Decimal>();

        for(CustomObject__c obj :[SELECT Serialized_Data__c FROM CustomObject__c WHERE Important_Field__c = :identifier]){
            result.putAll((Map<String, Decimal>)JSON.deserialize(obj.Serialized_Data__c, Map<String, Decimal>.class));
        }

        return result;
    }
}

Implementing an interface doesn't restrict you from adding additional methods, and since my main concern is with number of queries (and I can trade some heap space to reduce # of queries), there's nothing stopping me from pulling my query into another method and populating a static map.

public class MyCacheBuilder implements Cache.CacheBuilder{
    private static Map<String, Map<String, Decimal>> level2Cache = new Map<String, Map<String, Decimal>>();

    public void preLoadCache(List<String> identifiers){
        for(CustomObject__c obj :[SELECT Important_Field__c, Serialized_Data__c FROM CustomObject__c WHERE Important_Field__c IN :identifiers]){
            if(!MyCacheBuilder.level2Cache.containsKey(obj.Important_Field__c){
                MyCacheBuilder.level2Cache.put(obj.Important_Field__c, new Map<String, Decimal>());
            }
            MyCacheBuilder.level2Cache.putAll((Map<String, Decimal>)JSON.deserialize(obj.Serialized_Data__c, Map<String, Decimal>.class));
        }
    }

    // If we end up calling this method, it means we have a cache miss in
    //   the first level cache (platform cache)
    public Object doLoad(String identifier){
        Map<String, Decimal> result = new Map<String, Decimal>();

        // Before we resort to burning a query, check to see if our
        //   second level cache has our data
        if(MyCacheBuilder.level2Cache.containsKey(identifier){
            return MyCacheBuilder.level2Cache.get(identifier);
        }

        // As a last resort, we query
        for(CustomObject__c obj :[SELECT Serialized_Data__c FROM CustomObject__c WHERE Important_Field__c = :identifier]){
            result.putAll((Map<String, Decimal>)JSON.deserialize(obj.Serialized_Data__c, Map<String, Decimal>.class));
        }

        return result;
    }
}

The preLoadCache() method would need to be called before trying to use the cache, but that's pretty much what was going to need to happen one way or the other.

3
  • If I'm reading this right - if you were to reference MyCacheBuilder in a cache call before calling preLoadCache you would end up putting data into the cache that is then duplicated by the preLoadCache (I.e. put into the implied level1 cache?) Probably not an issue, as you say that you will always call preLoadCache in advance - but just to make sure I'm understanding this correctly. Mar 3, 2022 at 13:49
  • 1
    @RobBaillie That does appear to be a weakness in my implementation (doLoad() doesn't add missing data to the level2Cache, and preLoadCache() would query and overwrite the data if we did call it later). When fetching from a cache using a CacheBuilder though, the doLoad() method would only run (and take data from the level2Cache) if the key used in the cache .get() call is evicted from the level 1 cache (the org or session CachePartition). My main concern here was in reducing the number of possible queries.
    – Derek F
    Mar 3, 2022 at 14:10
  • Makes perfect sense. Thanks for the swift response Mar 3, 2022 at 15:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .