Tuesday, September 1, 2009

How to write a trigger in Salesforce.com






Often, in salesforce.com, you want to run some custom code just when some data is being inserted, updated or deleted to/from the database. Luckily, this is easy to achieve in a trigger; the syntax for declaring a trigger is fairly trivial:


trigger {TriggerName} on {SObject} ( {events} ) {
}


- Specify a name for the trigger
- Specify which SObject this trigger will run against (e.g. AccountOpportunityCustomObject__c, etc...)
- Specify when the trigger will fire. It can be:
  • before insert
  • after insert
  • before update
  • after update
  • before delete
  • after delete

Before we look into the syntax of creating a robust salesforce.com trigger, there's an important concept to understand - that of Governor Limits. Essentially - salesforce is multi-tenanted; that is to say there are many different customers’ databases running on the same servers. Salesforce cannot risk some rogue script running away with the CPU and grinding everyone to a screeching halt, so they impose governor limits. You physically cannot write an infinite loop; you cannot get more than 1000 rows from a query; you cannot run more than 20 queries in a trigger, the list continues...
Now, the next thing to understand is that your trigger code will run when you, say, insert a single record from a page; Also, the same code will run when you perform a batch insert (e.g. via the Apex Data Loader), so you may well be passing a collection of 200 items into your trigger...
So let's say we have a fairly simple scenario where we want to update an Account record with some Contact data (where 1 Account can have many Contacts). The Account object(s) we are updating will be passed into the trigger via a handy property called Trigger.new, so we will write something like:


trigger UpdateAccountFromContacts on Account (before insert, before update) {
  for (Account a : trigger.new) {
   // Loop through each Account being inserted / updated and do work here...
  }
}


So let's say we are trying to populate a field like 'Most recent contact' on the Account, where we mark the name of one of the Contacts (for simplicity sake, let's take the Contact record that was most recently modified). 
As we loop through the Accounts, we interrogate the variable 'a' and see that is has a Property called Contacts, of type Contact[]. Now in an ideal world, the system would implement lazy-loading and we could simply use a.Contacts in our code, and the system would go get the data on an as-needs basis, but unfortunately this is not so; we will have to query the fields ourselves...
So we need to write a sub-query to get the Contact Name from the Contacts collection in the Account.. it will look like this:


SELECT (SELECT Name FROM Contacts ORDER BY LastModifiedDate DESC LIMIT 1) FROM Account


Now the temptation is to stuff that into our for-loop and job's done, we would have:


trigger UpdateAccountFromContacts on Account (before insert, before update) {
  for (Account a : trigger.new) {
    Account accFromDb = [SELECT (SELECT Name FROM Contact ORDER BY LastModifiedDate DESC LIMIT 1) FROM Account WHERE Id =: a.Id];
    if (accFromDb.Contacts.size() > 0) {
      a.MostRecentContact__c = accFromDb.Contacts[0].Name;
    }
  }
}



But the problem with this is that if we are loading 200 Accounts from some batch loading process like the Apex data loader, then we are gong to attempt 200 select statements and the triggers going to throw a DML exception, because we can only hit the database 20 times in our trigger, remember?
So to get round this, instead of creating 200 selects where id = a.Id, we need to use the 'in' keyword so we can create a single select and get the records where Id in : (some list). It will look like this:


trigger UpdateAccountFromContacts on Account (before insert, before update) {
  // Use a Set, as add() method will only add an item if it is not already in the set, hence no duplicates...
  Set< Id > accountIds = new Set< Id >();
  for (Account a : trigger.new) {
    accountIds.add(a.Id);
  }
  Account accounts = [SELECT (SELECT Name FROM Contact ORDER BY LastModifiedDate DESC LIMIT 1) FROM Account WHERE Id in: accountIds];
}


So all that remains to be done is to dump the accounts (complete with Contact names) into a map, referenced by Account Id, then loop through the trigger.new Account list again, and setting the Account object being inserted/updated with recently-queried data from the db...
Our final trigger reads:


trigger UpdateAccountFromContacts on Account (before insert, before update) {
  // Use a Set, as add() method will only add an item if it is not already in the set, hence no duplicates...
  Set< Id > accountIds = new Set< Id >();
  for (Account a : trigger.new) {
    accountIds.add(a.Id);
  }
  Map accountMap = new Map([SELECT (SELECT Name FROM Contact ORDER BY LastModifiedDate DESC LIMIT 1) FROM Account WHERE Id in: accountIds]);
  for (Account a : Trigger.new) {
    Account accFromDb = accountMap.get(a.Id);
    a.MostRecentContact__c = accFromDb.Contacts[0].Name;
  }
}


So yes, it is slightly convoluted, and yes there is a CPU performance penalty, because we need to loop through the same collection *twice*.

These days in C#, we are using LINQ and lambda expressions to minimise our usage of costly foreach loops, and here we are in Apex doing the exact same loop twice!!

But the gain, of course, is that we have removed (up to) 199 calls to the database, which from a performance perspective more than makes up for it (and also allows us to play nicely in the multi-tenanted servers over in San Francisco)

9 comments:

  1. Thanks,

    Been asked by the boss today to learn triggers, this has been extremely useful. Keep it up!!!
    Matt

    ReplyDelete
  2. IAN,
    In a before insert trigger will the ID get populated ? it will be NULL rt?

    Sree

    ReplyDelete
  3. You're using C# yet care about performance? Hm.

    ReplyDelete
    Replies
    1. I'm an idiot. Also, I don't know what the hell I'm talking about.

      Delete
  4. Generally I do not learn post on blogs, but I wish to say that this write-up very pressured me to check out and do so!
    Your writing taste has been amazed me. Thanks, very great article.
    Also visit my website - play Games Win real cash

    ReplyDelete
  5. Good ԁay! Do you use Twittеr? I'd like to follow you if that would be okay. I'm absolutеly enjoying yоuг blog anԁ look fοrwaгԁ to neω upԁаtes.


    Alѕo vіsit my wеbsite ... diamondlinks review

    ReplyDelete
  6. good work by the author.. see this link to get http://www.salesforcetraining.in/ to get some more ideas

    ReplyDelete