AccountProcessor, number of contacts field not getting updated - Answers - Salesforce Trailblazer Community
Trailblazer Community
Ask Search:
Vanita KedarVanita Kedar 

AccountProcessor, number of contacts field not getting updated

I have ssme, unable to update number of contacts field on any of the update or insert operation

trigger AccountProcessorTrigger on Account (before insert, before update) {
    //Only insert scenaio would take of new records added to the Account
  for(Account acc: Trigger.New)
    {
        list<id> lstAccIds = new list<id>();
        lstAccIds.add(acc.Id);
        //calling future apex method
        AccountProcessor.countContacts(lstAccIds);
    }
}

public class AccountProcessor{    

    public static void countContacts(  list<id>  lstAccIds) {
        system.debug('lstAccIds : '+lstAccIds);
        List<Account> lstOfUpdatedAcc = new List<Account>();
         list<account> lstAcc = new list<account>();
        
        lstAcc =[Select Id, Number_of_Contacts__c, (Select Id from Contacts) from Account where id=:lstAccIds];
        system.debug('lstAcc : ' +lstAcc);
        for(Account acc :  lstAcc){
            system.debug('');
            if(acc != null){
                system.debug('Inside if(acc != null)');
                Integer contCount = 0;
                if(acc.Contacts != null && acc.Contacts.size()> 0){
                    system.debug('Inside if(acc.Contacts != null && acc.Contacts.size()> 0)');
                    contCount = acc.Contacts.size();
                    acc.Number_of_Contacts__c = contCount;
                    system.debug('Number_of_Contacts__c : '+ acc.Number_of_Contacts__c);
                    lstOfUpdatedAcc.add(acc);
                }
            }
        }
        Update lstOfUpdatedAcc;
    }
}
Best Answer chosen by Vanita Kedar
Amnon KruviAmnon Kruvi
This is what I came up with:
public inherited sharing class AccountUtils {
    public static void rollupContacts() {
        //Iterate over records to discover which accounts need to be recounted
        //Create a default count of 0 for all these accounts
        Map<Id, Account> mapAccounts = new Map<Id, Account>();
        for (Integer i=0;i < Trigger.new.size();i++) {
            if (Trigger.isInsert || Trigger.isUndelete) {
                //Insert or undelete: Add the New account
                Id accountId = ((Contact)Trigger.new[i]).AccountId;
                mapAccounts.put(
                    accountId,
                    new Account(
                        Id = accountId,
                        Number_of_Contacts__c = 0
                    )
                );
            } else if (Trigger.isDelete) {
                //Delete: Add the Old account
                Id accountId = ((Contact)Trigger.old[i]).AccountId;
                mapAccounts.put(
                    accountId,
                    new Account(
                        Id = accountId,
                        Number_of_Contacts__c = 0
                    )
                );
            } else if (Trigger.isUpdate) {
                //Update: Only add accounts if the contact's account had changed
                Id newAccountId = ((Contact)Trigger.new[i]).AccountId,
                   oldAccountId = ((Contact)Trigger.old[i]).AccountId;
                if (newAccountId != oldAccountId) {
                    mapAccounts.put(
                        newAccountId,
                        new Account(
                            Id = newAccountId,
                            Number_of_Contacts__c = 0
                        )
                    );
                    mapAccounts.put(
                        oldAccountId,
                        new Account(
                            Id = oldAccountId,
                            Number_of_Contacts__c = 0
                        )
                    );
                }
            }
        }

        //Query the number of contacts per account
        for (AggregateResult ar : [SELECT AccountId, COUNT(Id) cnt 
							   FROM Contact 
							   WHERE AccountId IN :mapAccounts.keySet()
							   GROUP BY AccountId]) {
            //Fetch the account ID and number of contacts from the aggregate results
            Id accountId = (Id)ar.get('AccountId');
            Integer contacts = (Integer)ar.get('cnt');

            //Set the number of contacts on the account
            mapAccounts.get(accountId).Number_of_Contacts__c = contacts;
        }

        //Update the accounts
        Database.update(mapAccounts.values(), false);
    }
}

You'll need to call AccountUtils.rollupContacts(); from an after insert/update/delete/undelete.

All Answers

Piyush SinghalPiyush Singhal
Hello Vanita,

public class AccountProcessor {
  public static void countContacts(Set<id> setId) {
      List<Account> lstAccount = [select id,Number_of_Contacts__c , (select id from contacts ) from account where id in :setId ];
      for( Account acc : lstAccount ) {
           List<Contact> lstCont = acc.contacts ;
           acc.Number_of_Contacts__c = lstCont.size();
            }
      update lstAccount;
      }
}

You can try this code and take reference to the similar thread.
https://developer.salesforce.com/forums/?id=906F0000000D8hwIAC
https://salesforce.stackexchange.com/questions/180304/accountprocessortest-test-class-doesnt-appear-to-calling-the-accountprocesso

I hope it will help.
Thanks
Piyush
Vanita KedarVanita Kedar
would this code work for trigger built on Acccoun objects? instead of contacts
Vanita KedarVanita Kedar
I already seen this one.. need more inside 
 does triger code looks fine?
Amnon KruviAmnon Kruvi
Hi Vanita, 

While Piyush's code just cleans up what you already wrote, I believe there are a few other problems in this code, and that the whole approach is incorrect. I'll explain:
1. Your trigger code creates one list for each account, calling your method X amount of times, and therefore calling that many updates. If one were to update 100+ accounts in the same transacrion, this would crash. The idea is to create one list with ALL accounts, and to call the method once. 

2. You're in  a Before trigger, but using an update to commit changes to your account instead of modifying the records in Trigger.new.

3. You're loading all child contact records instead of performing an aggregate query, leading to higher memory usage and possibly crashing on large accounts. 

4. And most importantly, this trigger should not be on Account in the first place. There is no reason to recount contacts every time an account changes - that's a huge waste of resources. This trigger belongs on the Contact object, after create, update  delete, or undelete, and needs to create a list of account IDs to recount.
Vanita KedarVanita Kedar
i agree, hence inbtween reading your post here .. i had  tried last option i.e. 4th  one, but still no luck

this is another trigger written on Contacts to achieve the same results

trigger ContactAccountProcessorTrigger on Contact (after insert, after update) {
    
    List<id> listAccountIds = new List<id>();
    list<Account> listAccount = new list<Account>();
    list<Account> listToUpdateAccount = new list<Account>();
    
    for(Contact con: Trigger.new){
        listAccountIds.add(con.accountid);
    }
    
    listAccount=[select id, name, Number_of_Contacts__c from account where id=:listAccountIds];
    
    for(Account acc :listAccount){
        
        integer countContacts=0;
        
        if(acc != null){
            if(acc.Contacts != null && acc.Contacts.size() > 0){
                countContacts= acc.Contacts.size();
                Account updateAcc = new  Account();
                updateAcc.id=acc.Id;
                updateAcc.Number_of_Contacts__c=countContacts;
                listToUpdateAccount.add(updateAcc);
            }
        }
    }
    upsert listToUpdateAccount;
}
Amnon KruviAmnon Kruvi
This is much better, and while we can still improve it, the basic premise of counting contacts when a new contact is created should work with this code. How are you testing it? 
Vanita KedarVanita Kedar
i am testing through application..by adding and updating contacts through account
Vanita KedarVanita Kedar
how do we get counting in SOQL?
Amnon KruviAmnon Kruvi
Let's not get ahead of ourselves and try to get this to work. You're creating a new contact, can you verify that this trigger is running? Does the contact count field on your account stay empty, does it change to 0, or something else? 
Amnon KruviAmnon Kruvi
But for reference, we'll be using an aggregate query later:
SELECT AccountId, COUNT(Id) FROM Contact WHERE AccountId IN :list AccountId GRPUP BY AccountId
Vanita KedarVanita Kedar
after creation of contact field stays empty
Vanita KedarVanita Kedar
i tried this

List<Contact> contactList = new List<Contact>([SELECT AccountId, COUNT(Id) FROM Contact WHERE AccountId IN: accountList.id group BY accountList.id]);

but again says 

Variable does not exist: id
Vanita KedarVanita Kedar
this is updated whoe code

trigger ContactAccountProcessorTrigger on Contact (after insert, after update, after delete,after undelete) {
    
    List<Id> listAccIds = new List<Id>();
    List<Account> accountListToUpdate = new List<Account>();
    
    IF(Trigger.IsAfter){
        IF(Trigger.IsInsert || Trigger.IsUndelete){
            FOR(Contact c : Trigger.new){
                if(c.AccountId!=null){   
                   listAccIds.add(c.AccountId); 
                }
            }
        }
        IF(Trigger.IsDelete){
            FOR(Contact c : Trigger.Old){
                if(c.AccountId!=null){   
                   listAccIds.add(c.AccountId); 
                }
            }
        }
    }
    System.debug('#### listAccIds  = '+listAccIds);
    
    List<Account> accountList = new List<Account>([Select id ,Name, Number_of_Contacts__c, (Select id, Name From Contacts) from Account Where id in:listAccIds]);
    FOR(Account acc : accountList){
        List<Contact> contactList = new List<Contact>([SELECT AccountId, COUNT(Id) FROM Contact WHERE AccountId IN: accountList.id group BY accountList.id]);
        acc.Number_of_Contacts__c = contactList.size();
        accountListToUpdate.add(acc);
    }
    try{
        update accountListToUpdate;
    }catch(System.Exception e){
        
    }

}
Amnon KruviAmnon Kruvi
Oh, I think you forgot to query the related contacts in the query for the account fields. 
Amnon KruviAmnon Kruvi
There is no .  in there, just the list of account IDs. 
Vanita KedarVanita Kedar
didnt get it..what needs to be chnaged
Amnon KruviAmnon Kruvi
List<AggregateResult> counters = [SELECT AccountId, COUNT(Id) FROM Contact WHERE AccountId IN :listAccIds  GROUP BY AccountId];

But this is why I didn't want to skip ahead :) You're gonna need to change the rest of your code to use this query since it returns an AggregateResult, rather than an account. 
Vanita KedarVanita Kedar
:( any other option..I need fast work around..almost invested half a day
Vanita KedarVanita Kedar
data type of number of conatcs is number..thats is correct? issnt it?
Amnon KruviAmnon Kruvi
A quick workaround would be to use DLRS, or get one of us to just write the whole thing for you. I always try to teach rather than give out code, but won't really have time to write it until later today anyway.
Vanita KedarVanita Kedar
request to help with 2nd option... do you neeed envirmet details?
Amnon KruviAmnon Kruvi
No need, these are standard objects so I could work on my dev org. Just that I'm only going to be free to code in another hour and a half, depending on how this demo call goes.
Vanita KedarVanita Kedar
Thanks a ton.. this is going to be great help.. i will keep eye on updates from you...

Also further to this trigger to get number of contacts (Number_of_Contacts__c)  on the field added to account.

I would be implementing the same scenario to cover concepts using
imple Apex Class and methods to trigger
Future Methods + Quable Apex to trigger.
to update past data by using Batch and Schedulable Apex

Thanks again
Amnon KruviAmnon Kruvi
This is what I came up with:
public inherited sharing class AccountUtils {
    public static void rollupContacts() {
        //Iterate over records to discover which accounts need to be recounted
        //Create a default count of 0 for all these accounts
        Map<Id, Account> mapAccounts = new Map<Id, Account>();
        for (Integer i=0;i < Trigger.new.size();i++) {
            if (Trigger.isInsert || Trigger.isUndelete) {
                //Insert or undelete: Add the New account
                Id accountId = ((Contact)Trigger.new[i]).AccountId;
                mapAccounts.put(
                    accountId,
                    new Account(
                        Id = accountId,
                        Number_of_Contacts__c = 0
                    )
                );
            } else if (Trigger.isDelete) {
                //Delete: Add the Old account
                Id accountId = ((Contact)Trigger.old[i]).AccountId;
                mapAccounts.put(
                    accountId,
                    new Account(
                        Id = accountId,
                        Number_of_Contacts__c = 0
                    )
                );
            } else if (Trigger.isUpdate) {
                //Update: Only add accounts if the contact's account had changed
                Id newAccountId = ((Contact)Trigger.new[i]).AccountId,
                   oldAccountId = ((Contact)Trigger.old[i]).AccountId;
                if (newAccountId != oldAccountId) {
                    mapAccounts.put(
                        newAccountId,
                        new Account(
                            Id = newAccountId,
                            Number_of_Contacts__c = 0
                        )
                    );
                    mapAccounts.put(
                        oldAccountId,
                        new Account(
                            Id = oldAccountId,
                            Number_of_Contacts__c = 0
                        )
                    );
                }
            }
        }

        //Query the number of contacts per account
        for (AggregateResult ar : [SELECT AccountId, COUNT(Id) cnt 
							   FROM Contact 
							   WHERE AccountId IN :mapAccounts.keySet()
							   GROUP BY AccountId]) {
            //Fetch the account ID and number of contacts from the aggregate results
            Id accountId = (Id)ar.get('AccountId');
            Integer contacts = (Integer)ar.get('cnt');

            //Set the number of contacts on the account
            mapAccounts.get(accountId).Number_of_Contacts__c = contacts;
        }

        //Update the accounts
        Database.update(mapAccounts.values(), false);
    }
}

You'll need to call AccountUtils.rollupContacts(); from an after insert/update/delete/undelete.
This was selected as the best answer
Vanita KedarVanita Kedar
let me try this quickly
Vanita KedarVanita Kedar
thats working fine..thanks...i will accomodate the same code to all other asynchronous apex
Vanita KedarVanita Kedar
working fine with Apex class and Future method too but still geting following error

19:06:17:018 FATAL_ERROR System.NullPointerException: Attempt to de-reference a null object
19:06:17:000 FATAL_ERROR Class.AccountProcessorFutureMethod.countContacts: line 40, column 1

for the below line in prog
for (Integer i=0;i < Trigger.new.size();i++)
Vanita KedarVanita Kedar
Also its very slow to get field updated actual count when new contact is added to the account
Amnon KruviAmnon Kruvi
Well, since you're running it asynchronously the Trigger context disappears, so the code doesn't know what the records are, or what operation we performed. Also, async runs with a delay - that's just how it is, nothing we can do about it.

Any reason that you need to run this asynchronously? If so, then you'll need to make a list of all the account IDs *synchronously* and only perform the query/update in your future method.