The comments on your question thus far pretty much have the issue identified and the solution presented.
Your issue (well, the main one causing this error at least) is that your updateField()
method is updating Desk__c
records, and you call this method from a trigger on Desk__c
. This starts an infinite loop, which Salesforce is detecting and stopping (presenting you with the error you see).
A solution to this issue is to make a couple of simple changes
- Change your trigger to be
before update
.
- Pass records into your helper method instead of querying for them inside your method
- Remove the dml statement from your helper method
From the documentation on triggers (emphasis mine)
There are two types of triggers:
- Before triggers are used to update or validate record values before they’re saved to the database.
- After triggers are used to access field values that are set by the system (such as a record's Id or LastModifiedDate field), and to affect changes in other records, such as logging into an audit table or firing asynchronous events with a queue. The records that fire the after trigger are read-only.
Since you're looking to update Desk__c
records, and your helper is being called from a Desk__c
trigger, you can use a before
trigger to avoid needing to use DML (and thus avoid your infinite loop).
To be able to update records without using DML, you need to use the Desk__c
instances stored in either Trigger.new
or Trigger.newMap
.
trigger
Trigger DeskExample on Desk__c (before update){
// This trigger only runs before update for now, so there's no need to check
// Trigger.isBefore and Trigger.isUpdate.
// It's still good practice (and a very cheap and simple check), in my opinion,
// but I'll be omitting it here for brevity.
// Also, notice that I've removed the for loop here (it'll show up in updateDate() ).
// You certainly could keep the for loop in the trigger here, but my experience
// leads me to want to pass a collection of records to a method once (rather
// than sending a single record and calling the method many times).
// Trigger.new is, generally speaking, a List<SObject>.
// However, inside of a trigger, Salesforce has enough context to know that it's a
// collection of a more specific type (List<Desk__c> in this case).
DeskCX.updateDate(Trigger.new);
}
helper
public class DeskCX {
// The method signature here changed so it can accept a List<Desk__c>
// Doing things this way also means that you could test this method separately
// from testing your trigger.
// You'd still need to test your trigger, but if you were to test this method
// as an independent unit, the test setup would be very simple.
// You'd only need to create the Desk__c records in-memory (no DML or querying
// required for your test setup).
public static void updateDate(List<Desk__c> deskList){
// Since the deskList is being passed as a parameter, there's no need to
// query for desks.
// And here's that for loop that was previously in your trigger
for(Desk__c desk :deskList){
// Your other logic remains largely the same
if(desk.field1__c != null && desk.field2__c != null){
desk.field1__c = desk.field2__c;
}
// Just like queries inside of a loop, dml inside of a loop is bad
// The DML update here is not required, so I've removed it
}
// DML update is still not required, so don't go putting that here (outside of
// the loop) either.
}
}
The reason why this will work is because it takes advantage of 2 separate features of Apex.
First, passing a collection into a method allows you to change the values contained in the collection (relevant documentation). When the method (that was passed a collection) returns, the caller does see the changes that were made. To illustrate this:
public void myTestMethod(){
List<Desk__c> myDeskList = new List<Desk__c>();
for(Integer i = 0; i < 10; i++){
myDeskList.add(new Desk__c(field1__c = null));
}
// Debugging at this point will show you that all Desk__c records have a null value
// stored in field1__c
system.debug(myDeskList);
myOtherTestMethod(myDeskList);
// The modifications we made have persisted after the method finishes.
// Debugging here will verify that all records have a value of "Potato" in
// field1__c
system.debug(myDeskList);
}
public void myOtherTestMethod(List<Desk__c> deskList){
// Simply modify a field.
for(Desk__c desk :deskList){
desk.field1__c = 'Potato';
}
}
The second feature that we're taking advantage of is one that I mentioned earlier, the behavior of before
triggers. If you update values stored in either Trigger.new
or Trigger.newMap
in a before
trigger, those changes will be saved without requiring DML.
These two features can be combined. By passing trigger.new
into your method, it allows your method to make changes to the values stored in trigger.new
. By virtue of being a before
trigger, the changes made by your method (stored in trigger.new
) will be saved without DML.
The one 'gotcha' with this method is that you only ever update records that are triggered. That means that your data going forward will be correct, but old, incorrect data will remain incorrect. The way around this is to run some anonymous apex to send your old records through your trigger (simply calling update someList;
will work. Even if you aren't updating any data on those records, they will still go through any update triggers that are active).
// Filtering on LastModifiedDate should ensure that we don't repeatedly try to update
// the same records over and over.
// The 'LIMIT 5000' is there for safety. We can only DML 10,000 records in a
// transaction, and this gives you room for other triggers/code to work (if you
// have any).
// You may need to run this as anonymous apex more than once, depending on how
// many records you have.
update [SELECT Id FROM Desk__c WHERE LastModifiedDate < TODAY LIMIT 5000];
Trigger.new
collection not query every record in the org (could be millions) and then only process the first one.