Problem
You want to write a trigger that
creates a new record as part of its processing logic; however, that
record may then cause another trigger to fire, which in turn causes
another to fire, and so on. You don't know how to stop that recursion.
Solution
Use a static variable in an Apex class to avoid an infinite loop. Static variables are local to the
context of a Web request (or test method during a call to runTests()), so all triggers that fire as a result of a
user's action have access to it.
For example, consider the following
scenario: frequently a Salesforce.com user
wants to follow up with a customer the day after logging a call with
that customer. Because this is such a common use case, you want to
provide your users with a helpful checkbox on a task that allows them
to automatically create a follow-up task scheduled for the next day.
You can use a before insert trigger on Task to
insert the follow-up task, but this, in turn, refires the before insert trigger before the follow-up task is inserted.
To exit out of this recursion, set a static class boolean variable
during the first pass through the trigger to inform the second trigger
that it should not insert another follow-up task:
The following code defines the class with the static class
variable:
public class FollowUpTaskHelper {
// Static variables are local to the context of a Web request
// (or testMethod during a runTests call)
// Therefore, this variable will be initialized as false
// at the beginning of each Web request which accesses it.
private static boolean alreadyCreatedTasks = false;
public static boolean hasAlreadyCreatedFollowUpTasks() {
return alreadyCreatedTasks;
}
// By setting the variable to true, it maintains this
// new value throughout the duration of the request
// (or testMethod)
public static void setAlreadyCreatedFollowUpTasks() {
alreadyCreatedTasks = true;
}
public static String getFollowUpSubject(String subject) {
return 'Follow Up: ' + subject;
}
}
The following code defines the trigger:
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>();
for (Task t : Trigger.new) {
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.getFollowUpSubect(followUpTask.subject);
if (followUpTask.ActivityDate != null) {
followUpTask.ActivityDate =
followUpTask.ActivityDate + 1; //The day after
}
followUpTasks.add(followUpTask);
}
}
FollowUpTaskHelper.setAlreadyCreatedFollowUpTasks();
insert followUpTasks;
}
}
The following code defines the test methods:
// This class includes the test methods for the
// AutoCreateFollowUpTasks trigger.
public class FollowUpTaskTester {
private static integer NUMBER_TO_CREATE = 4;
private static String UNIQUE_SUBJECT =
'Testing follow-up tasks';
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);
}
insert tasksToCreate;
System.assertEquals(NUMBER_TO_CREATE,
[select count()
from Task
where subject = :UNIQUE_SUBJECT
and ActivityDate = :System.today()]);
// Make sure there are follow-up tasks created
System.assertEquals(NUMBER_TO_CREATE,
[select count()
from Task
where subject =
:FollowUpTaskHelper.getFollowUpSubject(UNIQUE_SUBJECT)
and ActivityDate = :System.today()+1]);
}
static testMethod void assertNormalTasksArentFollowedUp() {
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 = false);
tasksToCreate.add(newTask);
}
insert tasksToCreate;
System.assertEquals(NUMBER_TO_CREATE,
[select count()
from Task
where subject=:UNIQUE_SUBJECT
and ActivityDate =:System.today()]);
// There should be no follow-up tasks created
System.assertEquals(0,
[select count()
from Task
where subject=
:FollowUpTaskHelper.getFollowUpSubject(UNIQUE_SUBJECT)
and ActivityDate =:(System.today() +1)]);
}
}
Recipe Activity - Please Log in to write a comment
Works like a charm for bulk DML for > 200 records
To get around the problem stated by jochen5478, I actually started using static Maps to track which records had already been processed and only processing them if they aren't in the Map(s). This is certainly not a completely safe solution but at least records in the "Subsequent" batches are being processed. The problem with this approach is that if you really have a lot of records, you can very quickly reach scripting and/or storage limits.
I agree with jochen5478 this pattern is not bulk safe at all and by implication there does not appear to be a workable solution to handling recursive triggers for large amounts of data. You can demonstrate the problem using the sames code posted in this solution and using the dataloader to insert more than 200 tasks. In my case I tried adding in 300 tasks, expecting to see 300 follow up tasks created, but only 199 follow up tasks were created. This is due to the fact that the trigger processes records in batches of 200 so after the first 200 are processed the flag hasAlreadyCreatedFollowupTasks is set. Each subsequent batch of 200 records that the trigger processes is in the same process where the hasAlreadyCreatedFollowupTasks is still set to true and as a result no follow up tasks are created for these batches. This is a major weakness with platform development and should really be looked into resolving.
There is a problem with handling batches. If batch size is over 100 the batch is splited into parts of 100 or lower. For each subbatch trigger code is executed but stays in the same context (so governer limits count for both subbatches). But the static variable is not reset! By this behaviour you may have run your trigger code for the first 100 records but not for the last 100 records of the batch!
This problem is barely documented by salesforce but I think this is a big issue for developers need to cope with recrusive triggers!
Sorry about that - I've fixed the repeating code.
actually this work around doesn't work properly in a test method context because the static survives across "DML contexts". So if you do another DML Operation on the same object in the same test method it will think that the trigger has already been invoked.
Even worse trigger SOQL count is NOT reset by test.startTest as documented.
I just noticed that as well too. All three code blocks are the same.
The trigger example code and the test example code are the same as the class example code.