Here is my solution. It works dynamically on all fields of certain types (I have tested with String, Picklist, Date, DateTime, Integer, Boolean). You could extend it so that not only updates, but also inserts are tracked. Edit: For some weeks, this code has been working in production without problems.
Be aware that it can compromise security because changes to fields are tracked even if the user cannot see the field on the page layout.
The object for which the history information will be kept is called Compliance__c
; the history is stored in ComplianceHistory__c
.
This is inside the after update trigger for Compliance__c
:
//get all fields from compliance that we want to check for changes
Map<String, Schema.SObjectField> allComplFieldsMap = Schema.SObjectType.Compliance__c.fields.getMap();
complFieldsToTrack = new Map<String, Schema.DescribeFieldResult>();
for (Schema.SObjectField complField : allComplFieldsMap.values()) {
Schema.DescribeFieldResult describeResult = complField.getDescribe();
//choose which fields to track depending on the field type
if (describeResult.getType() == Schema.DisplayType.Boolean ||
describeResult.getType() == Schema.DisplayType.Combobox ||
describeResult.getType() == Schema.DisplayType.Currency ||
describeResult.getType() == Schema.DisplayType.Date ||
describeResult.getType() == Schema.DisplayType.DateTime ||
describeResult.getType() == Schema.DisplayType.Double ||
describeResult.getType() == Schema.DisplayType.Email ||
describeResult.getType() == Schema.DisplayType.Integer ||
describeResult.getType() == Schema.DisplayType.MultiPicklist ||
describeResult.getType() == Schema.DisplayType.Percent ||
describeResult.getType() == Schema.DisplayType.Phone ||
describeResult.getType() == Schema.DisplayType.Picklist ||
describeResult.getType() == Schema.DisplayType.String ||
describeResult.getType() == Schema.DisplayType.TextArea ||
describeResult.getType() == Schema.DisplayType.Time ||
describeResult.getType() == Schema.DisplayType.URL)
{
//don't add standard fields that are not necessary
if (describeResult.getName() != 'CreatedDate' &&
describeResult.getName() != 'LastModifiedDate' &&
describeResult.getName() != 'SystemModstamp' &&
//only add fields that are visible to the current user
describeResult.isAccessible() &&
//do not add formula fields
!describeResult.isCalculated()
)
{
complFieldsToTrack.put(describeResult.getName(), describeResult);
}
}
}
The following is done for every object in Trigger.new inside a loop:
Compliance__c oldCompl = (Compliance__c)oldSo;
Compliance__c newCompl = (Compliance__c)so;
for (Schema.DescribeFieldResult fieldDescribe : complFieldsToTrack.values()) {
if (oldCompl.get(fieldDescribe.getName()) != newCompl.get(fieldDescribe.getName())) {
ComplianceHistory__c complHistory = createUpdateHistory(fieldDescribe, oldCompl, newCompl);
historiesToInsert.add(complHistory);
}
}
Here is the method to populate the history object referenced in the loop:
private ComplianceHistory__c createUpdateHistory(Schema.DescribeFieldResult field, Compliance__c oldCompl, Compliance__c newCompl) {
ComplianceHistory__c complHistory = new ComplianceHistory__c();
complHistory.Compliance__c = newCompl.Id;
complHistory.Event__c = 'Edit';
complHistory.Field__c = field.getLabel();
complHistory.User__c = UserInfo.getUserId();
// shorten strings that are longer than 255 characters (can happen if the field has the type textArea)
if (complHistory.OldValue__c != null) complHistory.OldValue__c = complHistory.OldValue__c.abbreviate(255);
if (complHistory.NewValue__c != null) complHistory.NewValue__c = complHistory.NewValue__c.abbreviate(255);
complHistory.EditDate__c = System.now();
return complHistory;
}
In the end, the history records are inserted:
if (!historiesToInsert.isEmpty()) {
//remove duplicate history entries
List<ComplianceHistory__c> historiesToInsertWithoutDuplicates = new List<ComplianceHistory__c>();
Set<ComplianceHistory__c> historiesSet = new Set<ComplianceHistory__c>();
historiesSet.addAll(historiesToInsert);
historiesToInsertWithoutDuplicates.addAll(historiesSet);
//insert the rest
insert historiesToInsertWithoutDuplicates;
}