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
IndyRIndyR 

Controlling recursive triggers - Handling errors in batches

I'm seeing a limitation in terms of controlling recursive triggers when the allOrNone database option is set to false and then the trigger causes an error to be added to a record in the batch.

 

To illustrate the limitation, I have used the example from the guidance for controlling recursive triggers described on the following page:

 

http://www.salesforce.com/docs/developer/cookbook/Content/apex_controlling_recursive_triggers.htm

 

I have made some minor modifications to this example. I updated the AutoCreateFollowUpOnTasks trigger to have the following modification to simulate the failure of a business rule for one record during a bulk insert:

 

Integer taskCount = 0;

for (Task t : Trigger.new) {
    // Fake an rule failure
    if (taskCount++ == 1)
    {
        t.addError('Test error');
    }
    else if (t.Create_Follow_Up_Task__c) {

 

Then I changed the testCreateFollowUpTasks method to have the following code:

 

Database.insert(tasksToCreate, false);

 

When I execute that test method, I can see that three tasks are inserted rather than the four that would have been inserted before my change. I expect that.  But I also see that no follow-up tasks are created. I would have expected that three follow-up tasks would be created.

 

It seems like when the allOrNone option is set to false and a trigger causes an error to be added to a record, then the trigger is re-run with the successful records without the static variables being reset. Is that right? If so, is there anything that can be done to handle recursive triggers in a way that would account for that situation?

crop1645crop1645

Your assertion is not true -- the static variables will retain their value throughout the transaction context. Please post all your code so we can assist.

IndyRIndyR

My code is the same as shown on http://www.salesforce.com/docs/developer/cookbook/Content/apex_controlling_recursive_triggers.htm with the following differences:

 

trigger AutoCreateFollowUpTasks on Task (before insert) {

    // Before cloning and inserting the follow-up tasks,  
    // make sure the current trigger context isn't operating  
    // on a set of cloned follow-up tasks.  
    
    if (!FollowUpTaskHelper.hasAlreadyCreatedFollowUpTasks()) {

        List<Task> followUpTasks = new List<Task>();
        Integer taskCount = 0;
        
        for (Task t : Trigger.new) {

            // Fake an arbitrary business rule failure

            if (taskCount++ == 1)
            {
                t.addError('Aribitrary error');
            }
            else if (t.Create_Follow_Up_Task__c) {

                // False indicates that the ID should NOT  
                // be preserved  
    
                Task followUpTask = t.clone(false);
                System.assertEquals(null, followUpTask.id);

                followUpTask.subject = 
                FollowUpTaskHelper.getFollowUpSubject(followUpTask.subject);
                if (followUpTask.ActivityDate != null) {
                    followUpTask.ActivityDate =
                      followUpTask.ActivityDate + 1; //The day after  

                }
                followUpTasks.add(followUpTask);
            }
        }
        FollowUpTaskHelper.setAlreadyCreatedFollowUpTasks();
        insert followUpTasks;
    }
}

 

    static testMethod void testCreateFollowUpTasks() {
        List<Task> tasksToCreate = new List<Task>();
        for (Integer i = 0; i < NUMBER_TO_CREATE; i++) {
            Task newTask = new Task(subject = UNIQUE_SUBJECT,
                    ActivityDate = System.today(),
                    Create_Follow_Up_Task__c = true );
            System.assert(newTask.Create_Follow_Up_Task__c);
            tasksToCreate.add(newTask);
        }

        Database.insert(tasksToCreate, false);
        System.assertEquals(3,
                            [select count()
                             from Task
                             where subject = :UNIQUE_SUBJECT
                             and ActivityDate = :System.today()]);

        // Make sure there are follow-up tasks created  
    
        System.assertEquals(3,
           [select count()
            from Task
            where subject = 
           :FollowUpTaskHelper.getFollowUpSubject(UNIQUE_SUBJECT)
           and ActivityDate = :System.today()+1]);
    }

 

crop1645crop1645

Hmm

 

  1. Your before insert trigger does 
insert followUpTasks

 Do you see this executing in the debug log -- that is, you should see DML for the original three Tasks and another DML for the followup tasks? I would expect you would.

 

2. The static variable set by 

FollowUpTaskHelper.setAlreadyCreatedFollowUpTasks()

 only controls the runaway creation of followup tasks to followup tasks

 

3. This leads me to think that your last system assertion is not fetching the correct rows. Is there any chance you have some other trigger/workflow that modifies task subject or activity date

System.assertEquals(3,
           [select count()
            from Task
            where subject = 
           :FollowUpTaskHelper.getFollowUpSubject(UNIQUE_SUBJECT)
           and ActivityDate = :System.today()+1]);

 

4. Stylistically, I always use method addDays(1) to add days to a date rather than adding integer '1'

 

5. I don't know why SFDC did this in their example but best practices should do DML of related records in the after trigger, not the before trigger - this way, only the SObjects that actually were successfully inserted/updated will trigger follow-on DML to other SObjects 

 

6. Best practice in testmethod when doing Database.xxx statements is to use

Database.SaveResult[] srList = Database.insert(tasksTocreate,false);
// Loop through srList[] and verify isSuccess() on each row that you expect to be successfully inserted

 

 

IndyRIndyR

Thanks for looking into this, Eric.

 

Regarding point #3.  To eliminate the chance of issues with the SOQL, I added the following assert to my test case.

 

 

System.assertEquals(6, [select count() from Task]);

 This fails with "expected: 6, actual: 3" indicating the follow-up tasks are not created.

 

Regarding point #1:  I added some debugging code in the method that checks the static variable:

 

public static boolean hasAlreadyCreatedFollowUpTasks() {
    System.debug('hasAlreadyCreatedFollowUpTasks: ' + alreadyCreatedTasks);
    return alreadyCreatedTasks;
}

 In the logs I see this (shortened for clarity):

 

07:39:46.598 (3598820000)|CODE_UNIT_STARTED|[EXTERNAL]|01pL000000055WQ|FollowUpTaskTester.testCreateFollowUpTasks
07:39:46.601 (3601344000)|DML_BEGIN|[21]|Op:Insert|Type:Task|Rows:4
07:39:46.740 (3740149000)|CODE_UNIT_STARTED|[EXTERNAL]|01qL00000004NEW|AutoCreateFollowUpTasks on Task trigger event BeforeInsert for [new, new, new, new]
07:39:46.745 (3745540000)|USER_DEBUG|[16]|DEBUG|hasAlreadyCreatedFollowUpTasks: false
07:39:46.751 (3751716000)|DML_BEGIN|[39]|Op:Insert|Type:Task|Rows:3
07:39:46.756 (3756236000)|CODE_UNIT_STARTED|[EXTERNAL]|01qL00000004NEW|AutoCreateFollowUpTasks on Task trigger event BeforeInsert for [new, new, new]
07:39:46.756 (3756512000)|USER_DEBUG|[16]|DEBUG|hasAlreadyCreatedFollowUpTasks: true
07:39:46.756 (3756607000)|CODE_UNIT_FINISHED|AutoCreateFollowUpTasks on Task trigger event BeforeInsert for [new, new, new]
07:39:48.275 (5275756000)|DML_END|[39]
07:39:48.276 (5276052000)|CODE_UNIT_FINISHED|AutoCreateFollowUpTasks on Task trigger event BeforeInsert for [new, new, new, new]
07:39:48.429 (5429314000)|CODE_UNIT_STARTED|[EXTERNAL]|01qL00000004NEW|AutoCreateFollowUpTasks on Task trigger event BeforeInsert for [new, new, new]
07:39:48.429 (5429625000)|USER_DEBUG|[16]|DEBUG|hasAlreadyCreatedFollowUpTasks: true
07:39:48.429 (5429716000)|CODE_UNIT_FINISHED|AutoCreateFollowUpTasks on Task trigger event BeforeInsert for [new, new, new]
07:39:48.496 (5496022000)|DML_END|[21]
07:39:48.505 (5505927000)|CODE_UNIT_FINISHED|FollowUpTaskTester.testCreateFollowUpTasks

 

I believe the section highlighted in red is where the trigger is being re-run with the existing static variables rather than reset static variables.

crop1645crop1645

IndyR

 

The debug log trace you sent up to the red lines is as expected. You can see the 3 followup tasks inserted at 07:39:46.751

 

You can also see that hasAlreadyCreatedFollowUpTasks: true remains true once set, even at 07:39:48.429 (5429625000). This is also as expected, static variables persist for the length of the transaction (testmethod in this case).

 

It is not clear to me why the three followup tasks are triggered twice unless you have some workflow rule that causes the Tasks to be modified somehow (and thus the before trigger will refire) - see Apex Developers Guide - Trigger Order of Execution

 

It is also not clear to me why you don't have 6 tasks since there is successful DML for 3+3

 

I'll go back to an earlier remark which is that you should not do explicit DML in before triggers - do in after triggers; try restructuring your code this way.

 

 

IndyRIndyR

Thanks for continuing to look at this, Eric.

 

I did have a workflow firing, but I believed it didn't have an impact on what was happening.  To be sure, I repeated my test in a brand new developer edition sandbox and I received the same results as shown in the log above. 

 

In the multitenancy white paper there's these lines:

 

When a bulk operation starts in partial save mode, the engine identifies a known start state and then attempts to execute each step in the process (bulk validate field data, bulk fire pretriggers, bulk save records, etc.). If the engine detects errors during any step, the engine rolls back offending operations and all side effects, removes the rows that are responsible for the faults, and continues, attempting to bulk process the remaining subset of rows.

 

http://www.developerforce.com/media/ForcedotcomBookLibrary/Force.com_Multitenancy_WP_101508.pdf

 

I'm starting to think that the platform rolls back side effects in terms of DML but not in terms of static variables.

crop1645crop1645

IndyR

 

Hmm, this is interesting.  There is no way SFDC could roll back static variables as they persist through the transaction context.  

 

I've never run into this phenomenon - probably because I never do explicit DML in a before trigger - I use after triggers instead - which, as described above in an earlier remark, avoid the partial update issue altogther as only successfully saved records will be triggered in the after update/insert trigger.

 

The static variables will retain their value even across the before | after trigger "boundary".