25

I've become quite comfortable with writing bulkified triggers that compare before and after values of specific fields. I've bumped into two cases where I'd like to do this generically for all fields. I don't want to depend on knowing the field's name and adding to the soql query and if-else block every time I add another field. Is there a way to generically walk a fields collection on an sObject? Is there a way to make sure that I've queried for all fields?

My two use cases:

(1) A trigger on product update that creates a diff email detailing all of the changes that were made (fieldName: beforeValue => afterValue).

(2) A trigger on opportunity that allows a very small subset of changes to Closed Won opportunities. So I have to walk all the fields and complain if any before values are different than their after values for fields that weren't specifically white listed.

Obviously, I could itemize all the fields in product and opportunity, build a massive soql for each and a massive if/else for each. But it would be a maintenance nightmare with adding and deleting custom fields. So I'm shopping for a smarter way to do it. Thanks in advance!

20

You need to combine schema describe (to know what fields are available) and dynamic apex (to work with fields that you may or may not know about when you write your code):

trigger OpportunityTrigger on Opportunity (after update) {
    Map<String, Schema.SObjectField> schemaFieldMap = Schema.SObjectType.Opportunity.fields.getMap();
    for(Opportunity newrec : Trigger.new)
    {
        Opportunity oldrec = Trigger.oldMap.get(newrec.Id);
        for(String fieldName : schemaFieldMap.keySet())
        {
            // put some filtering logic here - you don't need to compare all fields

            if(newRec.get(fieldName)!=oldRec.get(fieldName))
            {
                // note - fieldName isn't friendly - get the label from the schemaFieldMap
                newRec.addError(fieldName + ' was changed from ' + oldRec.get(fieldName) + ' to ' + newRec.get(fieldName));
                // we fail on first difference - if you want to report them all, you will need to handle this differently
                break; 
            }
        }
    }
}
5

Salesforce has something called "Dynamic Apex"

It allows you to handle records as generic sObjects... or you can get the information for a specific object.

Examples: You can use Dynamic apex to get the "Describe" for an objectType (Account.getDescribe()) With that describe you can get all the fields for that object. You can then loop through those fields to put it in a string to use with database.query(querystring) to query all fields.

Another example for dynamic Apex is to "put" value of a field. If you know the 'field token' you can change the value of that field. This means your code is object/field agnostic and you're passing into the code the name for the field.

take a look here: http://www.salesforce.com/us/developer/docs/apexcode/index.htm

I'll dig through my code and see if I can find a good example. I'll post it here Update

Quick Overview: Here I'm looping through contract records. i have fields that are in the format of "ProductFamily_Total_Recurring_Amount__c" -- what you don't see in this sample is that I built a list of my Product families by using dynamic apex to get the picklist values for the product family field on products.

So i build my field name by grabbing a value of a product family and turning the fieldName string into "MyProduct_Total_Recurring_Amount__c". I can then get the field that is' in "mFields" which is the map I built at the top. I could just as easily use that map to build a comma separate string of all the fields for database.query. Once I have the Field defined (f) I can replace the value by "putting" a new value. delegate.put(f,0.0); delegate is the contract record I'm currently updating.

  //Variables for Contract Loop
    String RecurringField = '_Total_Recuring_Amount__c';
    string NonRecurringField = '_Total_Non_Recurring_Amount__c';
    Decimal RecurringValue = 0.0;
    Decimal NonRecurringValue = 0.0;
    Object o = null; //Variable of field object that we can get the value of field name
    //Build map of all fields on Contract
    Map<String, Schema.SObjectField> mFields = new Contract().getSobjectType().getDescribe().fields.getMap();    
    //Dynamic Apex variables. 
    sObject delegate; //Contract active in loop    
    Schema.SobjectField f; //Field in contract being updated
    string FieldName = null; //Field API name. Used to get Field from mFields (Field Map)
    //Loop through Contracts
    for(integer i=0; i<TheseContracts.size(); i++)
    {
      RecurringField = '_Total_Recurring_Amount__c'; //Standard Ending. FieldName format is *Product*_Total_Recurring_Amount__c
      NonRecurringField = '_Total_Non_Recurring_Amount__c';//Standard Ending. FieldName format is *Product*_Total_Non_Recurring_Amount__c
      delegate = TheseContracts[i]; //Set the current contract in loop to the delegate sObject

      //Default Exisiting Contract Amount fields to 0
      //Loop through all ProductFamily Possibilities and grab field if it exists
      for(integer p=0; p<ProductFamilies.size(); p++)
      {
        //Default Recurring Field
        system.debug('Getting Product Field Reference from: ' + ProductFamilies[p].Replace('_',' '));
        ThisProdFieldRef = lProdFieldRefs.get(ProductFamilies[p].Replace('_',' '));
        system.debug('Resulting Product Field Reference: ' + ThisProdFieldRef);        
        //Build Recurring Field Name
        system.debug('Product Reference: ' + ThisProdFieldRef);
        if(ThisProdFieldRef != null){          
          FieldName = ThisProdFieldRef.Contract_Recurring__c;
          system.debug('FieldName set from Custom Setting: ' + FieldName);
        }else{
          FieldName = ProductFamilies[p] + RecurringField; //Build API name for field
        }
        f = mFields.get(FieldName); //Get the field from map of fields based on API Name
        system.debug('Default field: ' + FieldName + ' Return from FieldMap: ' + f);
        //Check to see if F is null. Null means field does not exist
        if(f != null)
        {
          delegate.put(f, 0.0); //Set the value of the field to 0
        }
        //Default Non Recurring Field
        if(ThisProdFieldRef != null){
          FieldName = ThisProdFieldRef.Contract_Non_Recurring__c;
          system.debug('FieldName set from Custom Setting: ' + FieldName);
        }else{
          FieldName = ProductFamilies[p] + NonRecurringField; //Build API name for field
        }
        f = mFields.get(FieldName); //Get the field from map of fields based on API Name
        //Check to see if F is null. Null means field does not exist
        if(f != null)
        {
          delegate.put(f, 0.0); //Set the value of the field to 0
        }        
      //End Prodct Family Loop
      }
3

As long as the sObject that you want to compare fields for is the one in your trigger you do not need to write any SOQL queries. Trigger.new and trigger.old already have all the fields on that sObject.

For the first use case the method you are looking for is:

Schema.SObjectType sObjectType
Map<String,Schema.Sobjectfield> fieldmap = sObjectType.getDescribe().fields.getMap()

The describe methods will return all the fields on an object but you want to make sure you are caching your results as describe calls are expensive and you don't want to do any more than necessary.

Your second use case doesn't require a trigger at all. What I would do is either

a) create a validation rule on the opportunity to enforce this logic. You would have to change if new user-editable fields are created but this is a fairly minimal amount of effort.

or b) create a 'locked' opportunity recordtype which opportunities are automatically covnerted to when they are 'closed/won' and create a page layout for that recordtype with everything read-only except the few fields people need to be able to edit.

EDIT: new answer below

If you do this with the JSON parser you do not have to use any describe calls and you do not have to walk through every field in the SObject.

public set<string> diff(Sobject obj, Sobject obj2)
{
    set<string> fieldDiff = new set<string>();

    map<string,object> objmap = (map<string,object>) Json.deserializeuntyped(JSON.serialize(obj));
    map<string,object> obj2map = (map<string,object>) Json.deserializeuntyped(JSON.serialize(obj2));
    objmap.remove('attributes');
    obj2map.remove('attributes');

    for(string key : objmap.keyset())
    {
        if( objmap.get(key) != obj2map.get(key)) fieldDiff.add(key); 
    }

    return fieldDiff;
}

In a trigger you would still compare every field but if you got the Sobject from a SOQL query or were instantiating the object in apex you would only compare the fields that were returned from your query or the fields you set on the object.

  • 1
    Thanks for the schema code and answering my second question. I see how (b) could work--but I'm paranoid about blocking changes from data loaders or elsewhere. Because I'm only allowing changes to 3 fields, I think (a) would require checking that the other 97 fields haven't changed which is what I'm trying to avoid. – twamley Oct 19 '12 at 20:46

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.