Logging Deleted Records in Salesforce.com

Note: The code for all triggers, classes, and pages can be downloaded as an archived Eclipse IDE Project from here, or you can install this as a package into your Development or Sandbox environment from here.

The Business Issues:

  • As an administrator, there are times when I wanted to monitor records deleted, especially for certain users who had a habit of inadvertently or even purposely deleting records.
  • The Recycle bin does not list Opportunities and Contacts deleted when their parent account is deleted. There were times when I was contacted to search for the “missing” Contact or Opportunity, but it was nearly impossible to locate because the Account associated with the record was deleted and the Contact/Opportunity did not show up in the Recycle bin.

The Solution:

  • Create Apex Triggers on key objects in SalesForce.com to log to a custom object when records were Deleted and Undeleted. To log deletes on your custom objects, simply use the IDE to create new Trigger on your custom object and then copy the trigger code from one of the existing standard object triggers. You’ll also need to modify the DeleteLog_Test class to add test coverage for your new trigger. The test code can likely just be copied for an existing standard object and then modified with your object name.
  • Build logic into the code to also log deleted child records, especially for Contacts and Opportunities.  It is fairly straight forward to extend this logic to log other child records such as Contracts or Assets by modifying the query in the commitInsert() method in the DeleteLog class.

Technical Issues:

  • Avoid hitting the maximum number of SOQL and Script Execution Statement limits in bulk deletes, such as when using the “Mass Delete Accounts” function. After some experimenting, the key to avoiding this issue involved re-writing the processing of Notes/Attachments/Tasks for records being deleted so that it first read in all of the items in a single query and then processed them in a more efficient loop using Map[]’s instead of Lists to avoid using a For loop to process the QueryResults in List[] form.
  • Creating a Test class for the Visual Force page.  I kept getting an error about no constructor[] for the DeletedLog_Extension class. From what I read on the developer boards there is  an issue with testing controller extensions. Luckily, there was a solution to the problem posted by mtbclimber.  http://community.salesforce.com/sforce/board/message?board.id=Visualforce&message.id=695#M695.
  • Optionally avoid logging of huge mass deletes by the administrator or by integrated applications.   In my past role as an Administrator there were occasions where I would mass-delete a bunch of records, almost always to correct an import or Outlook Add-in sync problem with a user. We also had a couple of integrated applications that would upload and sometimes mass-delete hundreds of records on a daily basis. To avoid logging all of these in the DeleteLog table, there is a custom field  added to the User object to allow the logging functionality to be disabled for a specific userID.  If you install the package or use the archived IDE project for this, be sure to manually add the “Do Not Log Deleted Records” field to the User Page Layout.

The base Trigger logic is fairly simple.

trigger Account_LogDelete on Account (before delete, after undelete) {
    DeleteLog LogDelete = New DeleteLog();
    if (Trigger.isDelete) {
        for (Integer i = 0; i < Trigger.old.size(); i++) {
            LogDelete.LogDeletedRecord(Trigger.old[i].Name,
            Trigger.old[i].Id,Trigger.old[i].getSObjectType().
            getDescribe().getLabel(), '' );
        }
        LogDelete.CommitInsert();
    }
    if (Trigger.isUnDelete) {
        for (Integer i = 0; i < Trigger.new.size(); i++) {
            LogDelete.LogUnDeletedRecord(Trigger.new[i].Id);
        }
        LogDelete.CommitRecoveredUpdates();
    }
}

It starts by creating a new instance of the DeleteLog class and then passes in the records from the Trigger.old[] collection to the LogDeletedRecord() or LogUnDeletedRecord() method depending on the type of action taken by the user. Finally, it calls the appropriate Commit() method to write the new records to the custom DeleteLog__c object.

The DeleteLog class has four main methods:

  • logDeletedRecord()
  • logUnDeletedRecord()
  • commitInsert()
  • commitRecoveredUpdates()

The logDeletedRecord() method essentially adds a new item to a class list of type DeleteLog__c. It also adds the ID of the object being deleted to an oID list. This will be used later to query all child records for the records being deleted. One key part of the method is that it forces a commit() at 150 records. The maximum number of records that can be inserted at one time is 200.

public void logDeletedRecord(string oName, string oID, string oType, string oParentID) {
   DeleteLog__c deletedRecord = new DeleteLog__c(Name = oName,
    ObjectID__c = oID, ObjectType__c = oType);
   if (oParentID <> '') deletedRecord.ParentID__c = oParentID;
   newRecords.Add(deletedRecord);
   IDs.add(oID);
   if (newRecords.size() > 149) {
      CommitInsert();
      newRecords.clear();
      IDs.clear();
   }
}

 

The commitInsert() method is where the SOQL logic had to be more creative to avoid hitting the governor limits for bulk operations. commitInsert() has three main steps:

  1. Count the number of Activities, Notes, and Attachments associated with each record being deleted.
  2. Write the DeleteLog records to the DeleteLog__c object
  3. If the object(s) being deleted are Accounts, find Contacts and Opportunities associated with the Accounts and add them to the DeleteLog__c object as well.

Finally, I added VisualForce page associated with viewing the DeleteLog__c object with an [Undelete] button. Though it is true that the button could have also been done using a JavaScript button on the standard page layout. What is nice about looking at a Deleted record, such as a Deleted Account, from this page is that we can see the Account along with the deleted child records listed.

Screenshot of VisualForce Page