7

Say I want to insert a Contact with some field, and have a Trigger that populates an other field. As a developer it always felt odd that the other field value is inaccessible without re-querying the object.

Contact c = new Contact();
c.FirstName = 'Foo';

insert c;
/**
 * trigger ContactTrigger on Contact (before insert) {
 *     Trigger.new[0].LastName = 'Bar';
 * }
 */

System.debug(c.FirstName); //'Foo' (I set this one above, so the value is ok)
System.debug(c.LastName);  //null  (This other field would have to be 're-queried')

If you're not interested in the fields set inside a Trigger, this might only be a rare annoyance. Just a slight confuser that the Trigger context is not the same reference as the insert operand.

Suppose for developer convenience, we truly wanted to share those references and access fields without re-querying. I guess we could work towards it by overextending the domain layer concept:

Contact c = new Contact();
c.FirstName = 'Foo';

new ContactModel(contact).insert();
/**
 * public class ContactModel {
 *
 *     private Contact contact;
 *
 *     public ContactModel(Contact contact) {
 *         this.contact = contact;
 *     }
 *
 *     public void insert() {
 *         // before-insert logic here...
 *
 *         this.Contact.LastName = 'Bar';
 *         insert this.Contact;
 *
 *         // after-insert logic here...
 *     }
 * }
 */

System.debug(c.FirstName); //'Foo' (I set this one above, so the value is ok)
System.debug(c.LastName);  //'Bar' (I set other field in domain layer, is ok)

So with that we share the same reference. And we see the fields alright. But it comes at a cost:

  • must never use native insert or update (ironically, we could disable those with a trigger)
  • must write ceremony triggers to invoke the same logic outside Apex (for GUI / API operations)
  • we are reinventing a fundamental platform wheel (triggers) which gives me the heebie jeebies

Am about to open source a library dealing with this.

But is there an existing pattern or native solution?

6
  • I can envision a 'poor mans' solution that assigns the Contact or Contacts to a static class property. Then instead of requerying after the DML, the entity can just be fished off ContactModel.LastInserted (for example's sake) Dec 30, 2014 at 15:55
  • Makes sense for synchronous requests coming from Apex, but what about async requests? Now we have to write two different types of triggers to support async and sync transactions. Also, what about bulk data loads via the API coming from external tools? I can see why this wasn't handled by default from the platform, it would be too messy and complex. Also, as triggers are part of the database layer, I somewhat expect them to return the specific record being impacted, nothing less, nothing more.
    – drakored
    Dec 30, 2014 at 16:14
  • You got it @drakored - doing handstands and cartwheels to catch the async / API events is exhausting. Feels like a lot of ceremony for it to be a 'complete' solution. Dec 30, 2014 at 16:17
  • I wonder if this couldn't have been fixed simply though with a Trigger.isAsync and Trigger.isAPI. Then you could load the necessary relationships (if necessary), or ignore those requests in your trigger. Anyway, dreaming here. I'm interested in your solution. I haven't seen anyone else put together a pattern for this aside from the obvious bulkifying and re-pulling the records at the start of the trigger (tedious, lame).
    – drakored
    Dec 30, 2014 at 16:24
  • Wait a min, in your first example, you do an insert, then apply a Before Insert trigger?? In a Before trigger, you can modify the contents of trigger.new but nothing is saved to the database until after the insert operation occurs. So what's happening makes perfect sense to me.
    – crmprogdev
    Dec 30, 2014 at 17:05

2 Answers 2

7

I tend to use a cache for this purpose. The actual cache can be simple:

public class Cache {
    static Map<Id, SObject> records = new Map<Id, SObject>();
    public static void add(SObject data) {
        record.put(data.Id, data.clone(true, true, true, true));
    }
    public static void add(SObject[] data) {
        records.putAll(data.deepClone(true, true, true));
    }
    public static SObject[] get(Set<Id> recordIds) {
        Map<Id, SObject> temp = records.clone();
        temp.keySet().retainAll(recordIds);
        return temp;
    }
    public static SObject get(Id recordId) {
        return records.get(recordId);
}

Then, you simply use after-DML operations to store a copy of whatever you want:

trigger addContactsToCache on Contact (after insert, after update) {
    Cache.add(Trigger.new);
}

Finally, you can retrieve the data any time you want with either a single ID or a set of ID values.

My cache algorithm I wrote a while back was more complicated; it would query missing records if it found any were not in the cache, but if your goal is simply to have the data available outside the trigger context, static variables make this easy.

2
  • Like what you've done with the place, Brian! Reckon that anything much more would be overkill, and this serves nicely when you 'just need that handle'. Dec 30, 2014 at 23:20
  • I use a similar concept for caching record types in triggers (our triggers need clean-up massively, so we have a lot of record type filtering going on). To prevent multiple queries, we cache the record types for the sobject and it queries on demand. I didn't even think to repurpose it like this. Nice thinking!
    – drakored
    Dec 31, 2014 at 4:20
3

Been able to do some good noodling on this one in the last couple hours. With help from @ca_peterson and @andyinthecloud it's easy to recognize why Trigger and SObject are different references, necessarily and by design:

  1. Those Trigger.New (etc) collections have to be garbage collected and having them hang around and permeate outside the commit context doesn't lend itself to transactions.

  2. While we gotta re-query to see changes, it's taking for granted we don't have to pre-query the fields inside the trigger! All fields are populated for free. You can't have your cake and eat it :-)

  3. Down at the metal, running Apex from a database trigger isn't trivial. Magically busting out to a high level language from a trigger should be appreciated as a cool/powerful thing in itself, as is.

  4. Wrapping huge swathes of the Salesforce API (Database.xxx) is a bad idea likely to give birth to a leaky abstraction, notwithstanding the fact it doesn't account for Workflow Rules etc etc.

You must log in to answer this question.

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