function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
bakumbakum 

Can I write this trigger without tripping the governor limit on SOQL queries?

Hi,

Background: Customer is a travel agency.  They have a custom object called Reservation__c, and on that object are two fields, Guest_Name__c (lookup to Account), and Departure_Date__c (date).

They want to be able to report on the accounts that have not booked a reservation within the last six months.  I cannot get this with a straight report, so I had the bright idea to put a field on the Account called Most_Recent_Reservation__c (lookup to Reservation__c), and a workflow on Reservation__c.  When a Reservation__c is created or edited a workflow compares Reservation.Departure_Date__c and Account.Most_Recent_Reservation__r.Departure_Date__c and applies some logic to update Account.Most_Recent_Reservation__c with that Reservation ID or not.  The customer could then report off the Most_Recent_Reservation__c.Departure_Date__c and Bob's my Uncle.  The logic for when to update works.  The first problem is I can't do a cross object update with a workflow. 

So my second bright idea is to use the workflow to set a flag on Reservation__c, and trap for that flag with a trigger.  The trigger can then do the cross object update no problem.

The second problem is I've done this sort of thing before and run into governor limits when doing bulk updates. 

So, the question is can I write this trigger some way so as not to hit the governor limit?  Here's the trigger code:

trigger update_most_recent_reservation on Reservation__c (after insert, after update)
{
    //make it bulk safe
    for (Integer i=0; i<Trigger.new.size(); i++)
    {
        Reservation__c res = Trigger.new[i];
        //check to see if the update flag is set to true
        if (res.Update_Guest_Most_Recnt_Resrvation_Flag__c)
        {
            //double check that it's appropriate to update the field
            if (res.Guest_Name__c != null && res.Departure_Date__c != null)
            {
                //find the right account to update and update it
                Account guest = [select Id from Account where Id = :res.Guest_Name__c];
                guest.Most_Recent_Reservation__c = res.Id;
                update guest;
            }
        }   
        //set the update flag on Reservation__c back to false
        res.Update_Guest_Most_Recnt_Resrvation_Flag__c = FALSE;
        update res;
    }
}

David VPDavid VP
If the relationship is master-detail, you could just create a roll-up summary field on Account with Max(reservationDate) of the Reservations and use that directly to report on.


-David
bakumbakum
Unfortunately SF isn't giving me the option of making it a M-D relationship.
David VPDavid VP
In that case, you need to move the following line out of your loop :


Account guest = [select Id from Account where Id = :res.Guest_Name__c];


To do that, first create a Set with all Guest_Name__c, then query for Accounts where Guest_Name__c is in the Set.
Drop the accounts in a Map with the Key the guestname and the value object the account.


Then in your loop you can get to the accounts via the Map, without having to query for them ...


Hope this works & helps :-)


-David
bakumbakum
david,

that's awesome but I don't quite understand...

Guest_Name__c is a lookup field, and a set only contains primitives...so something like this?
Set(Id) myAccounts = new Set<Id>(...I'm not sure what to put here...is it a query of all Guest_Name__c's?)

Then, I'm not sure how you got from the Set to the Map of Account objects. 

I guess what I don't understand is the Trigger has the field Guest_Name__c which is the Account ID that I need to update.  So I need to pull that out of the trigger.  But in order to update it, don't I need to do a query to get it into an object?

Thanks,
-mb

David VPDavid VP
If I understand all your fields and objects correctly then :

The Set you need to create contains the ID's of all the Guest_Name__c Id's that come in via Trigger.new.

Something like :
Code:
Set<Id> allguests = new Set<Id>();
for(Reservation__c res:Trigger.new) {
      allguests.add(res.Guest_Name__c); //perhaps Guest_Name__c.Id
}
 

Now you have a nice set of Id's that will allow you to get all the accounts that are relevant for the incoming Reservation__c, in just one query :

Code:
Map<Id, Account> accountsmap = new Map<Id, Account>([Select a.Id, a.Name From Account a where a.Id in :allguests]);

 Then in your loop instead of doing the query you just go :

Account guest = accountsmap.get(res.Guest_Name__c);


Edit :I just saw that there's one more DML operation in your loop : update res; (needs to get out of the loop too)
That's easy to solve : Instead of updating these, drop them in a list and do one update query with the list after your loop.


Hope this helps.


David



P.S. : Since you already have the Id of the Account in the res.Guest_Name__c, you might even try and not do the account Map query altogether. You could just in your loop create a new Account(), set the Id of that to Guest_Name__c, fill in the Most_Recent_Reservation__c field and update it (since you have the Id it will update the correct account and not touch the fields you didn't specify ... does this make sense ?)




Message Edited by David VP on 10-25-2008 11:23 AM

Message Edited by David VP on 10-25-2008 11:23 AM
bakumbakum
David,  I am 95% sure looking at this I understand it and it's awesome.  Thank you so much!
bakumbakum
Dave, if you don't mind one more question...

I'm getting an error I don't understand.  here's the new trigger:

trigger update_most_recent_reservation on Reservation__c (before update)
{
       
    Set<Id> allguests = new Set<Id>();
    //create a set of all the Guest_Name__c's in the trigger
    for(Reservation__c res:Trigger.new)
    {
      allguests.add(res.Guest_Name__c);
    }
   
    //use one query to create a map of the set above to actual account objects that we will use for updating.
    Map<Id, Account> accountsmap = new Map<Id, Account>([Select a.Id, a.Name From Account a where a.Id in :allguests]);
   
    // Create an empty list of String
    List<Account> myAccountUpdates = new List<Account>();
   
    //make it bulk safe
    for (Integer i=0; i<Trigger.new.size(); i++)
    {
        Reservation__c res = Trigger.new[i];
        //check to see if the update flag is set to true
        if (res.Update_Guest_Most_Recnt_Resrvation_Flag__c)
        {
            //double check that it's appropriate to update the field
            if (res.Guest_Name__c != null && res.Departure_Date__c != null)
            {
                //find the right account from the map
                Account guest = accountsmap.get(res.Guest_Name__c);
                //update that account's field
                guest.Most_Recent_Reservation__c = res.Id;
                //add that account to a list of accounts to update outside of the for loop
                myAccountUpdates.add(guest);
            }
            //set the update flag on Reservation__c back to false
            //before trigger so no DML needed
            res.Update_Guest_Most_Recnt_Resrvation_Flag__c = FALSE;
        }   
    }
   
    //now update accounts from list
    update myAccountUpdates;
}


here's the test method:
public class update_recent_res_flag_test {
    static testMethod void myTest()
    {
        Account guest = new Account();
        guest.FirstName = 'Jim';
        guest.LastName = 'Jones';       
        guest.RecordTypeId = '012400000005Os8';
        insert guest;
       
        Reservation__c res1 = new Reservation__c();
        res1.Name = '123456';
        res1.Guest_Name__c = guest.Id;
        res1.Departure_Date__c = Date.newInstance(2008,11,11);
        insert res1;
       
        Account guestTest = [select Id, Most_Recent_Reservation__c from Account where Id=:guest.Id];
        System.assertEquals(guestTest.Most_Recent_Reservation__c,res1.Id);
       
        Reservation__c resTest = [select Update_Guest_Most_Recnt_Resrvation_Flag__c
                               from Reservation__c
                               where Id=:res1.Id];
       
        System.assertEquals(resTest.Update_Guest_Most_Recnt_Resrvation_Flag__c,FALSE);
       
        Reservation__c res2 = new Reservation__c();
        res2.Name = '123457';
        res2.Guest_Name__c = guest.Id;
        res2.Departure_Date__c = Date.newInstance(2008,12,11);
        insert res2;
       
        guestTest = [select Id, Most_Recent_Reservation__c from Account where Id=:guest.Id];
        System.assertEquals(guestTest.Most_Recent_Reservation__c,res2.Id);
       
        res1.Departure_Date__c = Date.newInstance(2009,11,11);
        update res1;
       
        guestTest = [select Id, Most_Recent_Reservation__c from Account where Id=:guest.Id];
        System.assertEquals(guestTest.Most_Recent_Reservation__c,res1.Id);
           
               
    }

}


it generates this error:
System.DmlException: Update failed. First exception on row 0 with id 001R0000009ecnEIAQ; first error: INVALID_FIELD_FOR_INSERT_UPDATE, Account: bad field names on insert/update call: Name: [Name]

Trigger.update_most_recent_reservation: line 41, column 5 (which is this line: update myAccountUpdates;)

Obviously there are bad field names in my list, but I'm not sure how that's possible.  I'm only setting one field onto that list and it worked fine doing the DML udpate one by one in the for loop.  ANy suggestions?

-mb

P.S. I'm a PHP guy so this syntax is really a learning experience for me.  I really appreciate the help.
David VPDavid VP
I'm not seeing right away what the problem might be.

Do a system.debug(myAccountUpdates) and watch your system log window closely. It will show you the account list and all it's field values. You might get a clue from that.


-David
philbophilbo
Hey,

It looks to me...like...

your 'myAccountUpdates' array consists of Account records pulled from the 'accountsmap' map.  In each of these records, you are updating its Most_Recent_Reservation__c field.

But the 'accountsmap' map was created via a 'select Id , Name from Account...' SOQL statement.

In order to be able to modify a field on a record that you've pulled from the DB, you need to have included that field in your SOQL statement; i.e. your 'accountsmap' select should be 'select Id , Name , Most_Recent_Reservation__c...'.  Try that and see if it squashes your problem.
bakumbakum
I combined the last two suggestions and the problem has now "passed."

I used to be creating the map like this:
Map<Id, Account> accountsmap = new Map<Id, Account>([select a.Id,a.Name From Account a where a.Id in :allguests]);

then I tried like this:
Map<Id, Account> accountsmap = new Map<Id, Account>([select a.Id,a.Name ,a.Most_Recent_Reservation__c From Account a where a.Id in :allguests]);

but this actually worked:
Map<Id, Account> accountsmap = new Map<Id, Account>([select a.Id,a.Most_Recent_Reservation__c From Account a where a.Id in :allguests]);

and the problem has since disappeared.  I don't really know why but it has. 


Anyway, thanks to everyone who helped me.  I understand a LOT more about writing bulk safe triggers that actually do things now.  And that's awesome.