1

I am attempting to create a trigger to update the primary contact for an account if the contact has the max commission value. Here is what I have, I'm new to salesforce so it's not very good. The primary contact is a checkbox that should be checked if the contact has the max commissions within that account.

trigger updatePrimaryContact on contact (after insert, after update, before delete) {
    List<Id> accID = new List<Id>();
    Map<Id, account> parentAccs = new Map<Id, account>();
    for(contact c : trigger.new){
        Id a = c.accountId;
        list<contact> cList = [SELECT Id, Sum_of_Commissions__c ,Primary__c  FROM contact where AccountId = :a order by Sum_of_Commissions__c desc];
        contact oldcon = cList[0];
        if(c.Sum_of_Commissions__c > oldcon.Sum_of_Commissions__c){
            c.Primary__c = true;
            oldcon.Primary__c = false;

        System.debug('old' + oldcon.Primary__c);
        System.debug('new' + c.Primary__c);
        }
        else{
            oldcon.Primary__c = true;
            System.debug('old' + oldcon.Primary__c);
            System.debug('new' + c.Primary__c);
        }

    }
}

Here are the debug results for entering the below

Contact a = new Contact(LastName='Test Trigger');
insert a;

13:52:48:168 USER_DEBUG [16]|DEBUG|'old' true 13:52:48:168 USER_DEBUG [17]|DEBUG|'new' false

But it doesn't update any checkboxes when I edit contacts

2 Answers 2

0

Looks like the thing you missed here is the DML update operation. You also have a query inside of a loop (performing queries and DML inside of a loop is bad practice on the Salesforce platform. We're pretty restricted on the number of queries/DML that can be performed in a single transaction.)

Right now, you're changing the value of the record in memory only. That change isn't being persisted by saving it to, for lack of a better word, the database (which is what DML does).

In-memory changes to records are only automatically saved to the database without DML when:

  • You're working on records in Trigger.new or Trigger.newMap
  • In a before insert or before update trigger context

Your trigger is defined on after insert and after update contexts, and you're trying to update at least one record that isn't guaranteed to be in trigger.new or trigger.newMap, so you need to explicitly perform the DML (at least, with your code as-is).

The biggest concepts on customizing the Salesforce platform can be boiled down to:

  • Try to use clicks instead of code, where practical (using the provided "declarative" features. i.e. rollup summary fields, validation, workflow, formula fields, process builder)
  • Only have one trigger per SObject
  • In code, generally try to use and operate on collections (lists, sets, and maps) instead of individual records
  • Keep DML and Queries outside of loops

For one reason or another, we can't have a rollup summary field on Account that summarizes the related Contact records, so a purely declarative solution isn't possible1. I do, however, think you can save yourself a bit of trouble by making Primary__c a formula field.

The general idea is that you use a trigger to find the maximum value of Sum_of_Commissions__c for the contacts related to a given account, and then the formula field (on the Contact) looks to see whether or not its Sum_of_Commissions__c matches the maximum value that's stored on the account.

A quick and dirty example of that to get you started:

trigger X on Contact(after insert, after update, before delete){
    // To avoid performing a query in a loop, we typically gather things used in the WHERE
    //   clause in a collection (set, list, or map) by using a simple loop beforehand.
    Set<Id> accountIdSet = new Set<Id>();

    for(Contact c :trigger.new){
        accountIdSet.add(c.AccountId);
    }

    // Now we can perform the query we want to
    // By using the GROUP BY clause and Aggregate Functions, we can have SOQL do the work
    //   of figuring out what the max value should be
    // Using GROUP BY means we'll get a List of AggregateResult back rather than a List of 
    //   SObjects
    List<AggregateResult> results = [SELECT AccountId, MAX(Sum_of_Commissions__c) maxCommission FROM Contact WHERE AccountId IN :accountIdSet GROUP BY AccountId];

    // Now we can work on getting the accounts that we need to update
    // Use another collection to hold the accounts so we can do 1 DML operation after we've
    //   processed everything
    List<Account> accountsToUpdate = new List<Account>();
    for(AggregateResult ar :results){
        // Another helpful way to avoid queries is to use the SObject constructor to set
        //   the Id of an SObject.
        // As long as the SObject instance has an Id, we can perform a DML update on it.
        // A query would get you that Id as well, but queries are a precious resource.
        accountsToUpdate.add(new Account(
            // AggregateResult is a bit of a pain to work with. We need to use .get()
            //   to access each field, and it gives us a plain "Object" back that needs
            // to be type-cast
            Id = (Id)ar.get('AccountId'),
            Max_Commission__c = (Decimal)ar.get('maxCommission')
        );
    }

    // Done with all the account work, time to DML
    update accountsToUpdate;
}

From there, the formula field you'd put on Contact would be something simple like Sum_of_Commissions__c = Account.Max_Commission__c.

This approach could end up marking more than one Contact as "primary" (if more than one contact has the max value). Maybe you're ok with that. If not, then you should still be able to apply the concepts demonstrated in my example (gather Ids first, then query outside of all loops; gather records to be updated in a list, then perform the update outside of all loops; etc...)

1: There is Andrew Fawcett's Declarative Lookup Rollup Summary tool which provides a declarative interface to define rollups for more things than Salesforce give us by default. It automatically generates a trigger and test code. So it's declarative, but backed by code.

0

You have a query inside a loop; this is potentially bad, and unnecessary. Instead, you can do something like this:

trigger updatePrimaryContact on contact (after insert, after update, after delete, after undelete) {
    Map<Id, Contact> maxContacts = new Map<Id, Contact>();
    Map<Id, Contact> contactsToUpdate = new Map<Id, Contact>();
    for(Contact record: Trigger.new==null?Trigger.old:Trigger.new) {
      maxContacts.put(record.AccountId, null);
    }
    for(Contact record: [SELECT Sum_of_Commissions__c, Primary__c, AccountId FROM Contact WHERE AccountId = :maxContacts.keySet() ORDER BY Sum_of_Commissions__c ASC]) {
      maxContacts.put(record.AccountId, record);
      contactsToUpdate.put(record.Id, record);
    }
    Set<Id> tempIds = contactsToUpdate.keySet().clone();
    for(Id tempId: tempIds) {
      Contact temp = contactsToUpdate.get(tempId);
      Contact maxTemp = maxContacts.get(temp.AccountId);
      if(temp.Primary__c ^ maxTemp.Id != temp.Id) {
        temp.Primary__c = maxTemp.Id == temp.Id;
      } else {
        contactsToUpdate.remove(tempId);
      }
    }
    update contactsToUpdate.values();
}

The ^ operator is an XOR operator. It states that either condition must be true, but not both of them. This is equal to ((A&!B)|(!A&B)) (in other words, if primary and not maximum value, or not primary and is maximum value), we need to perform an update, otherwise we remove the contact from the list of records to update.

This is a recursive update, but it should never run more than twice. Note that this trigger should run after delete, not before delete, and should also run after undelete. This should ensure that the values are always correct for any given account.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .