Search code examples
triggerssalesforceapexsoql

Salesforce Trigger Update Error


Hello!

I am working on a trigger within Salesforce and I keep encountering an error that I can't seem to solve so I am hoping someone with more experience can help me get back on track. I've hunted around on Google and messed with the structure of my code many times but I'm unable to find an algorithm that works.

Purpose: I have been tasked with writing a trigger that will handle the logic required to maintain case rankings per developer. Each developer is assigned cases and those cases may or may not have a priority determined by the business. Each developer may only have 10 cases prioritized at any one time. Any other cases will just have a null value in the ranking field. If a case with a ranking is inserted, updated, or deleted then all the other cases assigned to that developer with a rank must automatically update accordingly. Any case with a rank higher than 10 will get nulled out.

Problem: I have completed the insert and the delete trigger functionality. That section of the code was tested and it functioned properly. I know it's possible that my code could have been written better so I will always accept advice in that area but the main thing I want to solve is the update logic within the trigger.

Errors:

  1. When I try to update a case with a null value rank so that it now has a value between 1-10 I get this error:

There were custom validation error(s) encountered while saving the affected record(s). The first validation error encountered was "Apex trigger CaseRankingTrigger caused an unexpected exception, contact your administrator: CaseRankingTrigger: execution of BeforeUpdate caused by: System.DmlException: Update failed. First exception on row 0 with id 500M0000006sLVOIA2; first error: SELF_REFERENCE_FROM_TRIGGER, Object (id = 500M0000006sLVO) is currently in trigger CaseRankingTrigger, therefore it cannot recursively update itself: []: Trigger.CaseRankingTrigger: line 62, column 1".

  1. When I try to update a case with an existing rank between 1-10 with a new number I get this error:

Error: Invalid Data. Review all error messages below to correct your data. Apex trigger CaseRankingTrigger caused an unexpected exception, contact your administrator: CaseRankingTrigger: execution of BeforeUpdate caused by: System.DmlException: Update failed. First exception on row 0 with id 500M0000006sLTrIAM; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, CaseRankingTrigger: execution of BeforeUpdate caused by: System.DmlException: Update failed. First exception on row 0 with id 500M0000006sLUaIAM; first error: SELF_REFERENCE_FROM_TRIGGER, Object (id = 500M0000006sLUa) is currently in trigger CaseRankingTrigger, therefore it cannot recursively update itself: [] Trigger.CaseRankingTrigger: line 74, column 1: []: Trigger.CaseRankingTrigger: line 62, column 1

  1. When I try to update a case with an existing rank to a null value, the case will null out but the other cases with values are not moved down to prevent a gap. Meaning if I have cases 1-8 and I delete case 5 then cases 6 7 and 8 should be moved down to 5 6 and 7 respectively.

Code:

trigger CaseRankingTrigger on Case (before insert, before update, before delete) {
// class level variables
Integer MAX = 10;
Integer MIN = 1;
String dev;
Decimal oldRank;
Decimal newRank;
/***************************************************************************************
* This block of code fires if a new Case is being inserted into the database
***************************************************************************************/
if (trigger.isInsert) {
    // iterates through the Cases in the new trigger
    for (Case c : trigger.new) {
        // sets the developer to the developer on the Case
        dev = c.Resource_Assigned__c;
        // sets the new rank for the Case being inserted
        newRank = c.Case_Rank__c;
        // this block of code only fires if the Case rank field is not null - this allows for Cases with no rank to be inserted without affecting any other Case
        if (newRank != null) {
            // populates a list of Cases assigned to the developer if they have a ranking equal to or greater than the rank of the new Case being inserted
            List<Case> devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND Case_Rank__c >= :newRank];
            // iterates over the list of Cases and increases their rank value by 1 to create room for the new Case then inserts that list back into the database
            for (Case devCase : devCases) {
                devCase.Case_Rank__c = devCase.Case_Rank__c + 1;
            }
            update devCases;
            // populates a list of Cases for the assigned developer if they have a ranking greater than the highest rank allowed
            devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND Case_Rank__c > :MAX];
            // iterates over the list of applicable Cases with a rank greater than 10 and nulls out the rank value then inserts that list back into the database
            for (Case devCase : devCases) {
                devCase.Case_Rank__c = null;
            }
            update devCases;
        }
    }
}
/***************************************************************************************
* This block of code fires if an existing Case is being updated
***************************************************************************************/
else if (trigger.isUpdate) {
    for (Case cOld : trigger.old) {
        oldRank = cOld.Case_Rank__c;
    }
    for (Case c : trigger.new) {
        dev = c.Resource_Assigned__c;
        newRank = c.Case_Rank__c;
        if (oldRank == null && newRank != null) {
            List<Case> devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND Case_Rank__c >= :newRank];
            for (Case devCase : devCases) {
                devCase.Case_Rank__c = devCase.Case_Rank__c + 1;
            }
            update devCases;
            devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND (Case_Rank__c > :MAX OR Case_Rank__c < :MIN)];
            for (Case devCase : devCases) {
                devCase.Case_Rank__c = null;
            }
            update devCases;
        } else {
            if (newRank != null) {
                List<Case> devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND Case_Rank__c >= :newRank];
                for (Case devCase : devCases) {
                    devCase.Case_Rank__c = devCase.Case_Rank__c + 1;
                }
                update devCases;
                devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND (Case_Rank__c > :oldRank OR Case_Rank__c < :newRank)];
                for (Case devCase : devCases) {
                    devCase.Case_Rank__c = devCase.Case_Rank__c - 1;
                }
                update devCases;
                devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND (Case_Rank__c > :MAX OR Case_Rank__c < :MIN)];
                for (Case devCase : devCases) {
                    devCase.Case_Rank__c = null;
                }
                update devCases;
            }
        }
    }
}
/***************************************************************************************
* This block of code fires if an existing Case is deleted from the database
***************************************************************************************/
else if (trigger.isDelete) {
    // iterates through the Cases in the old trigger
    for (Case c : trigger.old) {
        // sets the developer to the developer on the Case
        dev = c.Resource_Assigned__c;
        // sets the old rank value for the Case being deleted
        oldRank = c.Case_Rank__c;
        // this block of code only fires if the rank field is not null - this allows for Cases with no rank to be deleted without affecting any other Case
        if (oldRank != null) {
            // populates a list of Cases assigned to the developer if they have a ranking greater than the rank of the Case being deleted
            List<Case> devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND Case_Rank__c > :oldRank];
            // iterates over the list of applicable Cases and decreases their rank by 1 so there is no gap in rank priority then inserts that list back into the database
            for (Case devCase : devCases) {
                devCase.Case_Rank__c = devCase.Case_Rank__c - 1;
            }
            update devCases;
        }
    }
}
/***************************************************************************************
* Fall Through
***************************************************************************************/
else {}

}

I really appreciate any help you all can give me on this one!

Regards,

Michael


Final Update: After using the advice provided by @Egor I was able to get my code completed.

Trigger Code:

/***************************************************************************************
* @author:      Michael *REDACTED*
* @email:        *REDACTED*
* @date:         11/09/15
* @brief:         This is a trigger for the Case object that will modify the rank of the Cases
*                     assigned to the developer based on a priority set by the Administrator.
* @files:          src\classes\CaseRankTriggerHandler.cls
*                     src\classes\CaseRankTriggerHandlerTest.cls
*                     src\layouts\Case-Salesforce Service Ticket.layout
*                     src\objects\Case.object
*                     src\workflows\Case.workflow
***************************************************************************************/
trigger CaseRankTrigger on Case (before insert, before update, before delete) {
    /*
    * The CaseRankTriggerHandler constructor method takes (List<Case>, List<Case>, String)
    */
    if (trigger.isInsert) CaseRankTriggerHandler handler = new CaseRankTriggerHandler(trigger.new, trigger.old, 'Insert'); // if a new Case is being inserted into the database
    if (trigger.isUpdate) CaseRankTriggerHandler handler = new CaseRankTriggerHandler(trigger.new, trigger.old, 'Update'); // if an existing Case is being updated
    if (trigger.isDelete) CaseRankTriggerHandler handler = new CaseRankTriggerHandler(trigger.new, trigger.old, 'Delete'); // if an existing Case is deleted from the database
}

Trigger Handler Code:

/***************************************************************************************
* @author:      Michael *REDACTED*
* @email:        *REDACTED*
* @date:         11/09/15
* @brief:         This is a Case object trigger handler class that provides logic to the CaseRankTrigger for manipulating
*                     the ranks of all Cases assigned to a developer based on a priority that is set by an Administrator.
* @files:          src\classes\CaseRankTrigger.cls
*                     src\classes\CaseRankTriggerHandlerTest.cls
*                     src\layouts\Case-Salesforce Service Ticket.layout
*                     src\objects\Case.object
*                     src\workflows\Case.workflow
***************************************************************************************/
public with sharing class CaseRankTriggerHandler {
    // class level variables
    private static Boolean firstRun = true;
    private static Boolean modify = false;
    private static Integer MAX = 10;
    private static Integer MIN = 1;
    private List<Case> newTrigger {get; set;}
    private List<Case> currentTrigger {get; set;}
    private List<Case> cases {get; set;}
    private List<Case> newList {get; set;}
    private List<Case> currentList {get; set;}
    private String developer {get; set;}
    private Decimal newRank {get; set;}
    private Decimal currentRank {get; set;}
    /***************************************************************************************
    * @author:    Michael *REDACTED*
    * @email:      *REDACTED*
    * @date:       11/16/15
    * @brief:       Class constructor method.
    * @return:     Void
    ***************************************************************************************/
    public CaseRankTriggerHandler(List<Case> newT, List<Case> oldT, String type) {
        if (firstRun) { // makes sure that the trigger only runs once
            firstRun = false;
            InitializeTrigger(newT, oldT, type); // initializes the trigger
            if (developer != null) { // skips trigger if DML is performed on a Case with no developer assigned
                ModificationCheck(type); // determines if Cases need to be modified
                if (modify) ModificationLogic(type); // modifies Cases if needed
            }
        }
    }
    /***************************************************************************************
    * @author:    Michael *REDACTED*
    * @email:      *REDACTED*
    * @date:       11/16/15
    * @brief:       The InitializeTrigger method initializes the handler class based on the type of trigger fired.
    * @return:     Void
    ***************************************************************************************/
    private void InitializeTrigger(List<Case> newT, List<Case> oldT, String type) {
        if (type == 'Insert') {
            this.newTrigger = newT;
            this.developer = newTrigger[0].Resource_Assigned__c;
            this.newRank = newTrigger[0].Case_Rank__c;
            this.newList = [SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :developer AND Case_Rank__c != null AND Case_Rank__c = :newRank ORDER BY Case_Rank__c];
        } else if (type == 'Update') {
            this.newTrigger = newT;
            this.currentTrigger = oldT;
            this.developer = newTrigger[0].Resource_Assigned__c;
            this.newRank = newTrigger[0].Case_Rank__c;
            this.currentRank = currentTrigger[0].Case_Rank__c;
            this.newList = [SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :developer AND Case_Rank__c != null AND Case_Rank__c = :newRank ORDER BY Case_Rank__c];
            this.currentList = [SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :developer AND Case_Rank__c != null AND Case_Rank__c = :currentRank ORDER BY Case_Rank__c];
        } else if (type == 'Delete') {
            this.currentTrigger = oldT;
            this.developer = currentTrigger[0].Resource_Assigned__c;
            this.currentRank = currentTrigger[0].Case_Rank__c;
        }
    }
    /***************************************************************************************
    * @author:    Michael *REDACTED*
    * @email:      *REDACTED*
    * @date:       11/16/15
    * @brief:       The ModificationCheck method ensures various conditions are met, depending on the type
    *                   of trigger that was fired, before modifying the ranks of the Cases assigned to the developer.
    * @return:     Void
    ***************************************************************************************/
    private void ModificationCheck(String type) {
        if (type == 'Insert') {
            // the Case being inserted has a new rank not equal to null and if the assigned developer already has a Case with the
            // same rank as the new rank, we will proceed to modification, if not the record will be inserted without modification.
            if (newRank != null && !newList.isEmpty()) {
                modify = true;
            }
        } else if (type == 'Update') {
            // if the Case being updated has ranks with different values in both triggers we will proceed to the next check, if not the record is updated without modification.
            if (newRank != currentRank) {
                // if the Case being updated has a (new rank equal to null and a current rank not equal to 10) or
                // if the Case being updated has a new rank not equal to null, we will proceed to the next check,
                // if not the record is updated without modification.
                if ((newRank == null && currentRank != 10) || newRank != null) {
                    // if the assigned developer on the Case being updated already has a Case with the same rank as the new or current rank, we will proceed to modification,
                    // if not the record is updated without modification.
                    if (!newList.isEmpty() || !currentList.isEmpty()) {
                        modify = true;
                    }
                }
            }
        } else if (type == 'Delete') {
            // if the Case being deleted has current rank not equal to null, we will proceed to modification, if not the record is deleted without modification.
            if (currentRank != null) {
                modify = true;
            }
        }
    }
    /***************************************************************************************
    * @author:    Michael *REDACTED*
    * @email:      *REDACTED*
    * @date:       11/16/15
    * @brief:       If a Case rank needs to be updated the ModificationLogic method calls the appropriate
    *                   computation method based on trigger type and the values of newRank and currentRank.
    * @return:     Void
    ***************************************************************************************/
    private void ModificationLogic(String type) {
        if (type == 'Insert') {
            for (Case c : newTrigger) {
                // calls the IncreaseCaseRank method and passes it a list of Cases that are assigned to the developer that have a rank greater than or equal to the new rank.
                IncreaseCaseRank([SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Id NOT IN :newTrigger AND Resource_Assigned__c = :developer AND Case_Rank__c >= :newRank ORDER BY Case_Rank__c]);
            }
        } else if (type == 'Update') {
            for (Case c : newTrigger) {
                if (currentRank == null) {
                    // if the current rank is null - calls the IncreaseCaseRank method and passes it a list of Cases that are assigned to the developer that have a rank greater than or equal to the new rank.
                    IncreaseCaseRank([SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Id NOT IN :newTrigger AND Resource_Assigned__c = :developer AND Case_Rank__c >= :newRank ORDER BY Case_Rank__c]);
                } else if (newRank == null) {
                    // if the new rank is null - calls the DecreaseCaseRank method and passes it a list of Cases that are assigned to the developer that have a rank greater than the current rank.
                    DecreaseCaseRank([SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Id NOT IN :newTrigger AND Resource_Assigned__c = :developer AND Case_Rank__c > :currentRank ORDER BY Case_Rank__c]);
                } else if (newRank > currentRank) {
                    // if the new rank is greater than the current rank - calls the DecreaseCaseRank method and passes it a list of Cases that are assigned to the developer that have a rank less than or equal to the new rank and greater than to the current rank.
                    DecreaseCaseRank([SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Id NOT IN :newTrigger AND Resource_Assigned__c = :developer AND (Case_Rank__c <= :newRank AND Case_Rank__c > :currentRank) ORDER BY Case_Rank__c]);
                } else if (newRank < currentRank) {
                    // if the new rank is less than the current rank - calls the IncreaseCaseRank method and passes it a list of Cases that are assigned to the developer that have a rank a.
                    IncreaseCaseRank([SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Id NOT IN :newTrigger AND Resource_Assigned__c = :developer AND (Case_Rank__c >= :newRank AND Case_Rank__c < :currentRank) ORDER BY Case_Rank__c]);
                }
            }
        } else if (type == 'Delete') {
            for (Case c : currentTrigger) {
                // calls the DecreaseCaseRank method and passes it a list of Cases that are assigned to the developer that have a rank greater than the current rank.
                DecreaseCaseRank([SELECT Subject, CaseNumber, Case_Rank__c FROM Case WHERE Id NOT IN :currentTrigger AND Resource_Assigned__c = :developer AND Case_Rank__c > :currentRank ORDER BY Case_Rank__c]);
            }
        }
    }
    /***************************************************************************************
    * @author:    Michael *REDACTED*
    * @email:      *REDACTED*
    * @date:       11/16/15
    * @brief:       The DecreaseCaseRank method provides the logic required to properly
    *                   decrease or null out the ranks of the Cases assigned the the developer.
    * @return:     Void
    ***************************************************************************************/
    private void DecreaseCaseRank(List<Case> cases) {
        // if the list of Cases passed in by the ModificationLogic method isn't empty then it will iterate through the
        // list and decrease their ranks by 1 or null out the rank if it is not within the acceptable limits (1-10).
        if (!cases.isEmpty()) {
            for (Case c : cases) {
                if (c.Case_Rank__c >= 1 && c.Case_Rank__c <= 10) {
                    c.Case_Rank__c = c.Case_Rank__c - 1;
                } else {
                    c.Case_Rank__c = null;
                }
            }
            update cases;
        }
        return;
    }
    /***************************************************************************************
    * @author:    Michael *REDACTED*
    * @email:      *REDACTED*
    * @date:       11/16/15
    * @brief:       The IncreaseCaseRank method provides the logic required to properly
    *                   increase or null out the ranks of the Cases assigned the the developer.
    * @return:     Void
    ***************************************************************************************/
    private void IncreaseCaseRank(List<Case> cases) {
        // if the list of Cases passed in by the ModificationLogic method isn't empty then it will iterate through the
        // list and increase their ranks by 1 or null out the rank if it is not within the acceptable limits (1-10).
        if (!cases.isEmpty()) {
            for (Case c : cases) {
                if (c.Case_Rank__c >= 1 && c.Case_Rank__c < 10) {
                    c.Case_Rank__c = c.Case_Rank__c + 1;
                } else {
                    c.Case_Rank__c = null;
                }
            }
            update cases;
        }
        return;
    }
}

Solution

  • The problem is that you are trying to update the same case that already fired the trigger.

    devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND (Case_Rank__c > :MAX OR Case_Rank__c < :MIN)];
            for (Case devCase : devCases) {
                devCase.Case_Rank__c = null;
            }
            update devCases
    

    Your select statement will return the case that fired this trigger as well, because its Case_Rank__c is null and therefore meets the criteria. Same with the select you run for updating cases with a rank of 1-10.

    Try devCases = [SELECT Id, Case_Rank__c FROM Case WHERE Resource_Assigned__c = :dev AND (Case_Rank__c > :MAX OR Case_Rank__c < :MIN) AND Id NOT IN trigger.new];

    Check out https://help.salesforce.com/apex/HTViewSolution?id=000005278&language=en_US for a description of the exception being thrown