6

Use Case:

Need to allow changes to only one specific field on an opportunity when at a certain stage.

Problem:

How to efficiently determine within a trigger context that a only the specific field has changed and no other values have changed.

Info:

Salesforce's documentation on Sobject equality states that Sobject1 == Sobject2 will perform a field by field comparison of the two sobjects. Though my testing seems to indicate that Salesforce's is performing this field by field comparison with more fields than is returned by performing a metadata describe. As when I perform a metadata describe to get the Sobjects set of fields and iterate said set comparing the two Sobjects values I get a result indicating the two Sobjects are equal. Yet when I do the check of Sobject1 == Sobject2 it comes back false.

Questions:

Does a trigger context Sobject(as in the Sobjects in trigger.new) contain extra values that would cause an equality check to fail despite all metadata describe visible fields being equal?

Conversely does a Sobject clone contain extra values that would cause an equality check to fail despite all metadata describe visible fields being equal?

Why would the following not result as being equal assuming only the Opportunity Type field has changed?

Opportunity oldOppClone = trigger.old[0].clone(true,true,true,true);

oldOppClone.Type = trigger.new[0].Type;

system.assert(trigger.new[0]==oldOppClone);

Is there any other way to facilitate obtaining a set of fields which have changed between two Sobjects than metadata describing and iterating the fields and checking each individually?

2
  • Efficiently? Not possible.
    – Adrian Larson
    Commented Feb 12, 2018 at 21:06
  • Thanks all who responded sfdcfox's answer is the closest to what I went with which was metadata describe all fields and iterate to find the differences.
    – gussamer
    Commented Feb 20, 2018 at 22:52

2 Answers 2

4

The Trigger.old and Trigger.new values will always differ; even after running SObject.clone(false, false, false, false), which clears out autonumber fields, and the four audit fields, the SystemModStamp will survive the cloning process, meaning that a simple equality check (oldRecord == newRecord) will fail. A better idea would be to set up a Field Set, and check just the values in the field set against each other:

for(Integer index = 0, size = Trigger.new.size(); index < size; index++) {
  Opportunity oldRecord = Trigger.old[index], newRecord = Trigger.new[index];
  for(FieldSetMember m: Schema.Opportunity.fieldSets.someFieldSet.getFields()) {
    if(oldRecord.get(m.getFieldPath()) != newRecord.get(m.getFieldPath()) {
      // Something is different here

Edit: Having been reminded of this answer, there's a more efficient way now. Just get the fields you want with getPopulatedFieldsAsMap, trim down to just the fields you want, and then compare the maps directly.

String[] fields = new String[] { 'Name','Industry','AnnualRevenue' };
for(Integer index = 0, size = Trigger.new.size(); index < size; index++) {
  Map<String, Object> oldValues = Trigger.old[index].getPopulatedFieldsAsMap();
  Map<String, Object> newValues = Trigger.new[index].getPopulatedFieldsAsMap();
  oldValues.keySet().retainAll(fields);
  newValues.keySet().retainAll(fields);
  if(oldValues != newValues) {
    // Something is different here
  }
}

This eliminates the inner for loop, which can result in a drastic improvement in performance.

0
4

It will likely be inefficient agonizingly slow, but you might want to loop through the results of the SObject.getPopulatedFieldsAsMap() method.

public with sharing class ModificationTracker
{
    final Set<String> toIgnore = new Set<String>();
    public ModificationTracker ignore(SObjectField field)
    {
        return ignore(String.valueOf(field))
    }
    public ModificationTracker ignore(String field)
    {
        toIgnore.add(field);
        return this;
    }

    public List<SObject> getModified(List<SObject> records, Map<Id, SObject> records)
    {
        List<SObject> modified = new List<SObject>();
        for (SObject record : records)
        {
            if (isModified(newRecord, oldRecord))
            {
                modified.add(newRecord);
            }
        }
        return modified;
    }
    Boolean isModified(SObject newRecord, SObject oldRecord)
    {
        Map<String, Object> values = newRecord.getPopulatedFieldsAsMap();
        for (String field : values.keySet())
        {
            if (value != oldRecord.get(field))
            {
                return true;
            }
        }
        return false;
    }
}

I'm not sure if this method will ignore LastModifiedDate and other audit fields, you'd have to play around with the toIgnore blacklist to get the behavior correct.

The implementation would look like:

List<Opportunity> modifiedRecords = new ModificationTracker()
    .ignore(Opportunity.LastModifiedDate) // if necessary
    .ignore(Opportunity.CustomField__c)
    .getModified(trigger.new, trigger.oldMap);
1
  • getPopulatedFieldsAsMap is the way to go, but once you get there, you don't need the inner loops. In fact, the fastest way possible in Apex to compare many fields is the use of this method. I've edited that into my answer.
    – sfdcfox
    Commented Sep 13, 2022 at 13:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .