1

So basically, I'm struggle in this trigger. What I have to do is: get the max Commission Amount value of the contacts from an Account, check which is the largest and trigger a check box field called "Primary" as true (and uncheck the checkbox from the other contacts as well).

I'm really new on triggers and salesforce as well, and I know there is a lot of code missing here.

I tried to sort the commission values (I have already these values, because I created a custom object called Commissions to store each contact commission and the sum of all these values in the field Commission Amount).

(sorry for the bad english as well)

trigger CheckPrimary on Account (after update) {
    List<Contact> c = new List<Contact>();
    
    for(Account a: [SELECT Id, (SELECT Id, Name, Commission_Amount__c FROM Contacts 
        WHERE Commission_Amount__c != null ORDER BY Commission_Amount__c DESC
        LIMIT 1)FROM Account WHERE Id IN: Trigger.New]){
        Contact con = new Contact();
            con.Primary__c = true;
    }
}
1
  • Welcome to Salesforce Stack Exchange (SFSE)! What is happening when this code runs - Error? Unexpected/weird behavior? Nothing?
    – Moonpie
    Jun 28, 2022 at 10:58

1 Answer 1

1

I think that the code you have right now is actually pretty close. I'm assuming that Commission_Amount__c is just the commission for one Contact, not the sum of all commissions for all Contacts under a given Account.

The main thing I see is that this is a trigger that I would put on Contact rather than Account. The reason being that the thing you're interested in is Commission_Amount__c on the Contact.

The only times that a trigger on the parent object is fired when a child record changes are:

  • When you have a rollup summary field on the parent (which isn't possible between Account and Contact)
  • When you have code that explicitly performs DML on the parent

The query that you're using is pretty much spot on. Query the Accounts and have a parent-child subquery to isolate the largest value of Commission_Amount__c for each account by using ORDER BY and LIMIT 1.

To handle the second part, "uncheck the checkbox from the other contacts", you need to use a second query to find the Contact record(s) currently checked (so you can uncheck them).

So all said and done, your trigger would look like

// Trigger on Contact instead of Account
// Unfortunately, this isn't likely to be a situation where we can take advantage
//   of the benefits of a "before" trigger
trigger CheckPrimary on Contact (after update) {    
    // The generally accepted approach for getting parent record data is
    // 1) Aggregate Ids
    // 2) Perform your query
    // 3) Process and update records

    // One of the things you'll need to worry about is making sure you don't have
    //   duplicate records that you're trying to update in a single DML.
    // You could work around this by using two separate Lists, and two separate
    //   DML updates, but that's a bit of a waste.
    // Queries are our most precious resource, so if there's a way to reduce
    //   the number of queries we need to run it's something worth considering.
    // Performing extra DML can only cause the number of queries used to increase
    //   (or stay the same), so let's try to do this with a single DML update.
    Map<Id, Contact> contactsToUpdateMap = new Map<Id, Contact>();

    // Aggregate Ids
    Set<Id> accountIdSet = new Set<Id>();
    for(Contact c :trigger.new){
        accountIdSet.add(c.AccountId);
    }
    
    // Perform a query
    for(Contact c :[SELECT Id FROM Contact WHERE AccountId IN :accountIdSet AND Primary__c = true]){
        // Process records

        // There is a chance that the Primary Contact isn't changing
        // In that scenario, if we unset Primary__c first, then we can
        //   overwrite the record contained in the map to change Primary__c back
        //   to true.
        // If we did this the other way around, there would be a chance that no
        //   contact would be marked as Primary.
        c.Primary__c = false;
        contactsToUpdateMap.put(c.Id, c);
    }
        
    // Perform another query
    for(Account a: [SELECT Id, (SELECT Id, Name, Commission_Amount__c FROM Contacts 
        WHERE Commission_Amount__c != null ORDER BY Commission_Amount__c DESC
        LIMIT 1) FROM Account WHERE Id IN :accountIdSet]){
        // Process more records

        // To access child records from a parent-child subquery, you use the 
        //   "relationship name" (Contacts, in this case)
        // Child records are stored in an embedded List<SObject>
        // Normally, you'd want to take the safe approach and use a nested loop to
        //   access the child records, but since each Account will have a single
        //   child returned it is safe to access it directly
        contactsToUpdateMap.put(a.Contacts[0].Id, a.Contacts[0]);

        // Non-primitive types (things that aren't Integer, Boolean, String, etc...)
        //   are stored in collections (maps, lists, sets) as references.
        // If you change the record outside of the map, it'll also change the reference
        //   stored in the map
        a.Contacts[0].Primary__c = true;
    }

    // Since we're in an After Update trigger, we need to perform DML to persist
    //   our changes.
    // Even if this were a "Before Update" trigger, we can only avoid performing 
    //   DML for the records in trigger.new
    // We're probably going to work on records outside of those contained
    //   in trigger.new, so DML would be required anyway.
    update contactsToUpdateMap.values();
}

That's close, but it would get you into an infinite loop. The DML update at the end of the trigger would cause the Contact trigger to run again, which would perform another DML, and so on.

To avoid that, you'd need a recursion guard. The simplest recursion guard is just a static Boolean variable in an Apex class. Note that it does need to be an Apex class, because static variables declared in a trigger end up not being very static at all.

So the final result (with fewer comments) would look like

public class ContactRecursionGuard{
    public static Boolean isRunning = false;
}
trigger CheckPrimary on Contact (after update) {    
    // Check the recursion guard
    // Do an early "return" if we're already running so we don't loop
    //   infinitely
    if(ContactRecursionGuard.isRunning){ return; }

    Map<Id, Contact> contactsToUpdateMap = new Map<Id, Contact>();

    Set<Id> accountIdSet = new Set<Id>();
    for(Contact c :trigger.new){
        accountIdSet.add(c.AccountId);
    }
    
    for(Contact c :[SELECT Id FROM Contact WHERE AccountId IN :accountIdSet AND Primary__c = true]){
        c.Primary__c = false;
        contactsToUpdateMap.put(c.Id, c);
    }
    
    for(Account a: [SELECT Id, (SELECT Id, Name, Commission_Amount__c FROM Contacts 
        WHERE Commission_Amount__c != null ORDER BY Commission_Amount__c DESC
        LIMIT 1) FROM Account WHERE Id IN :accountIdSet]){
        contactsToUpdateMap.put(a.Contacts[0].Id, a.Contacts[0]);
        a.Contacts[0].Primary__c = true;
    }

    // This is the problematic statement, the one we have to be careful with
    // Set the recursion guard just before, and then un-set it immediately after
    ContactRecursionGuard.isRunning = true;
    update contactsToUpdateMap.values();
    ContactRecursionGuard.isRunning = false;
}

Another option, which would make your trigger a little shorter, remove a query, and get rid of the need to use a recursion guard would be to make Primary__c on Contact a formula field (of type checkbox).

If you store the current maximum contact commission on the Account (which is what you'd do with the trigger on Contact), you can reference that in a formula on Contact. You can traverse upwards in object hierarchies in the same way you can with a query. This is what's called a "cross-object formula".

Commission_Amount_c = Account.Max_Commission_Amount__c

That would take care of automatically un-checking the previous "primary" Contacts. The trigger would change slightly (you'd be updating Accounts instead of Contacts), but it would still look familiar.

trigger CheckPrimary on Contact (after update) {
    Map<Id, Account> accountsToUpdateMap = new Map<Id, Account>();

    Set<Id> accountIdSet = new Set<Id>();
    for(Contact c :trigger.new){
        accountIdSet.add(c.AccountId);
    }
    
    for(Account a: [SELECT Id, (SELECT Id, Name, Commission_Amount__c FROM Contacts 
        WHERE Commission_Amount__c != null ORDER BY Commission_Amount__c DESC
        LIMIT 1) FROM Account WHERE Id IN :accountIdSet]){
        a.Max_Commission_Amount__c = a.Contacts[0].Commission_Amount__c;
        accountsToUpdateMap.put(a.Id, a);
    }

    update accountsToUpdateMap.values();
}
1
  • Taking a look in your explanation, everything went absolutely clear!!! Thank you so much. By the way, the second option is really a way better. It also solve another step that I would have to do in the future after the trigger started to running: To make sure users couldn't manually edit the “Primary” checkbox. Putting the checkbox as a formula avoided this situation. Jun 28, 2022 at 13:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.