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!
Attribution to: twamley
Possible Suggestion/Solution #1
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
}
Attribution to: Salesforce Wizard
Possible Suggestion/Solution #2
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.
Attribution to: Greg Grinberg
Possible Suggestion/Solution #3
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;
}
}
}
}
Attribution to: Stephen Willcock
Possible Suggestion/Solution #4
As of Summer 16' the most efficient way to traverse all the populated fields of an SObject is to use Map<String, Object> getPopulatedFieldsAsMap()
Map<String, Object> fieldsToValue = a.getPopulatedFieldsAsMap();
for (String fieldName : fieldsToValue.keySet()){
System.debug('field name is ' + fieldName + ', value is ' +
fieldsToValue.get(fieldName));
}
Attribution to: NSjonas
This content is remixed from stackoverflow or stackexchange. Please visit https://salesforce.stackexchange.com/questions/3517