3

I have a trigger on User that after it's inserted or updated, I want a contact for that user to be created. For example, if a new user Fred is created, I also want a contact created with Fred's information. I know I need to use a future method because I am using multiple DML exceptions with the User object. It seems to be working in dev, but in production I am having an issue with it combining with a batch process. As a result I am getting UserInsertUpdate: System.LimitException: Too many future calls: 51. I tried to remedy this by adding the comparison so that it would only call the class with the future method if only certain fields were updated and not a batch method, but I keep getting the Too Many Future Calls error. While debugging, the comparison seems to be working that a future call is not run if one of those fields isn't updated, and only 1 is run if any of the listed fields are updated. Can someone tell me where in my code I'm allowing more than 50 future calls to be run in a single transaction to cause this error? Thanks in advance!

Update To clarify, my code seems to work, but we have a job running at 7am every day that is updating other fields on users. In my case it doesn't have anything to do with my code, but at the moment the trouble seems to be caused by the batch that's running. That's why I added the if(!System.isBatch()) as well as the comparisons, unfortunately what I had doesn't seem to be working.

Trigger

trigger UserInsertUpdate on User (after insert, after update) {  
      if(Trigger.isAfter && Trigger.isInsert){
            if(!System.IsBatch()){
                SyncContactsToUsers.sync(Trigger.newMap.keySet());
            }
        } else if(Trigger.isAfter && Trigger.isUpdate){
            if(!System.IsBatch()){
                for(User n : Trigger.new){
                    for(User o : Trigger.old){

                        if(n.isActive != o.isActive || n.LastName != o.LastName || n.Email != o.Email || n.Phone != o.Phone || n.True_Market_Name__c != o.True_Market_Name__c){
                            // Has to be in a different transaction to avoid a mixed DML exception.
                            SyncContactsToUsers.sync(Trigger.newMap.keySet());
                            System.debug('************************************************Trigger.new isActive: ' + n.isActive);
                            System.debug('************************************************Trigger.old isActive: ' + o.isActive);
                            System.debug('************************************************Trigger.new Phone: ' + n.phone);
                            System.debug('************************************************Trigger.old Phone: ' + o.phone);
                            System.debug('************************************************Trigger.newMap.keySet()' + Trigger.newMap.keySet());
                        }
                    }
                }
            }
        }        
}

Class

public class SyncContactsToUsers {

        public class SyncContactsToUsersException extends Exception {
        }

        @future
        public static void sync(Set<Id> userIds) {

            // Find existing related Contacts (if any)
            Map<Id, Contact> m = new Map<Id, Contact>();
            for (Contact c : [SELECT Id, Related_User__c FROM Contact WHERE Related_User__c in :userIds]) {
                m.put(c.Related_User__c, c);
            }

            // Update existing or insert new Contacts with values consistent with User 
            List<Contact> contacts = new List<Contact>();
            for (User u : [SELECT Id, FirstName, LastName, True_Market_Name__c, Email, Phone, isActive FROM User WHERE Id in :userIds]) {
                Contact c = m.get(u.Id);
                if (c == null) {
                    c = new Contact(Related_User__c = u.Id);
                }

                c.FirstName = u.FirstName;
                c.LastName = u.LastName;
                c.User_True_Market_Name__c = u.True_Market_Name__c;
                c.Email = u.Email;
                c.Phone = u.Phone;
                c.OwnerID = u.Id;
                c.AccountId = '001E000000GWdFN';
                c.Contact_Status__c = u.isActive ? 'Active' : 'Inactive';
                contacts.add(c);
            }

            try{
                upsert contacts;
            }
            catch(DMLException e){
                System.debug('The following exception has occurred: ' + e.getMessage());
            }
        }

    }
  • Are there any relevant Contact triggers that might update User? From a straight naming convention standpoint, SyncContactsToUsers is pretty misleading as it looks like you're going in the other direction. I would probably call it UsersToContacts so you can call UsersToContacts.sync(ids). – Adrian Larson May 29 '15 at 13:29
  • Maybe your trigger is recursively running? ave you tried adding Trigger.isExecuting to the logic? – James May 29 '15 at 13:31
4

Your change checking logic should collect a set of changed ids and then pass those ids in a single call to SyncContactsToUsers.sync. The double nested loop is not the appropriate way to do change checking and is one cause of your problems: instead have one loop and get the old values from Trigger.oldMap.

The replacement code would look like this:

trigger UserInsertUpdate on User (after insert, after update) { 
    if (Trigger.isInsert) {
        SyncContactsToUsers.sync(Trigger.newMap.keySet());
    } else if (Trigger.isUpdate) {
        Set<Id> userIds = new Set<Id>();
        for (User n : Trigger.new){
            User o = Trigger.oldMap.get(n.Id);
            if (n.isActive != o.isActive
                    || n.LastName != o.LastName
                    || n.Email != o.Email
                    || n.Phone != o.Phone
                    || n.True_Market_Name__c != o.True_Market_Name__c
                    ) {
                userIds.add(n.Id);
            }
        }
        if (userIds.size() > 0) {
            SyncContactsToUsers.sync(userIds);
        }
    }
}

PS

My mistake about @future not being allowed in the batch context. if simply dropping the use of @future altogether isn't possible, somehow presumably the logic also needs running in the batch case. Moving that responsibility out of the trigger makes sense. If not using @future in the batch case works then something like this could be done"

public class SyncContactsToUsers {

    public static void sync(Set<Id> userIds) {
        if (System.isBatch()) {
            syncNow(userIds);
        } else {
            syncInFuture(userIds);
        }
    }

    @future
    private static void syncInFuture(Set<Id> userIds) {
        syncNow(userIds);
    }

    private static void syncNow(Set<Id> userIds) {
        // Logic goes here
    }

    ...
}
  • You left out the check for batchable context. You can't future from a batch... – sfdcfox May 29 '15 at 13:51
  • Hey, @KeithC can you explain why changing around the change checking would fix this issue? What was my code doing that caused the issue? It seemed to be catching whether or not one of those fields was updated or not, within my debug logs. – Stephen May 29 '15 at 13:56
  • I think there's a little confusion with what I was asking. I don't want my future method to run if there's a batch running on user that doesn't impact one of my 6 fields that I have listed. So I guess I could use your updated version, and just put a simple System.debug in the SyncNow method? – Stephen May 29 '15 at 14:00
  • 1
    @Stephen With your code, take the case where say 5 Users are being updated at the same time. Each new User object is compared with all 5 old User objects and so a change will be identified for every new object for 4 out of the 5 old objects.. You are then passing all 5 object IDs into the sync method. So thats 5x4x5 = 100 calls to the sync method whether there are any changes or not. – Keith C May 29 '15 at 14:01
  • 1
    @Stephen The code I posted will only make 1 future call per set of User changes. The limit is on the number of calls not the number of objects affected by the call. The 1 future call will execute in its own single transaction a little while in the future. – Keith C May 29 '15 at 14:10
1

It seems that there is some issue with the update block in trigger. It needs to be bulkified. Try following code:

trigger UserInsertUpdate on User (after insert, after update) {  
      if(Trigger.isAfter && Trigger.isInsert){
            if(!System.IsBatch()){
                SyncContactsToUsers.sync(Trigger.newMap.keySet());
            }
        } else if(Trigger.isAfter && Trigger.isUpdate){
            if(!System.IsBatch()){
                List<Id> idToProcess = new List<Id>();
                for(User n : Trigger.new){
                    if(n.isActive != Trigger.oldMap.get(n.id).isActive || n.LastName != Trigger.oldMap.get(n.id).LastName || n.Email != Trigger.oldMap.get(n.id).Email || n.Phone != Trigger.oldMap.get(n.id).Phone || n.True_Market_Name__c != Trigger.oldMap.get(n.id).True_Market_Name__c){
                        idToProcess.add(n.Id);
                    }
                    if(idToProcess.size() > 0)
                        SyncContactsToUsers.sync(idToProcess);
                    /*for(User o : Trigger.old){

                        if(n.isActive != o.isActive || n.LastName != o.LastName || n.Email != o.Email || n.Phone != o.Phone || n.True_Market_Name__c != o.True_Market_Name__c){
                            // Has to be in a different transaction to avoid a mixed DML exception.
                            SyncContactsToUsers.sync(Trigger.newMap.keySet());
                            System.debug('************************************************Trigger.new isActive: ' + n.isActive);
                            System.debug('************************************************Trigger.old isActive: ' + o.isActive);
                            System.debug('************************************************Trigger.new Phone: ' + n.phone);
                            System.debug('************************************************Trigger.old Phone: ' + o.phone);
                            System.debug('************************************************Trigger.newMap.keySet()' + Trigger.newMap.keySet());
                        }
                    }*/
                }
            }
        }        
}

Hope this may help.

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.