14

QUESTION

I am trying to create a few utility methods that can work with any sObject that gets passed to it and return dynamic data-types. Unfortunately I'm starting to wonder, is this even possible?

EXAMPLE

Here is an example method I want for getting a list of sObject.Id from a Map<Id,List<sObject>> parameter.

public class Utils {

    public static List<object> getFieldListFromMap (Map<Id,List<sObject>> object_map, String field){
        List<object> objects = new List<object>();
        for(Id id : object_map.keyset()){                //traverse through map
            for (SObject obj : object_map.get(id) ) {    //traverse through List of sObects
                if(obj.get(field) != null)               //exclude null values
                    objects.add(obj.get(field));
            }
        }
        return objects;  
       // ^^ can I pass a string of the data-type and dynamically cast it?
    }

}

The hope from this is I can pass something like:

Map<Id,List<CampaignMember>> members; // a map collection of CampaignMembers by ContactID
List<Id> campaign_ids = Utils.getFieldListFromMap(members, 'CampaignId');

OR

Map<Id,List<Quote>> quotes; // a map collection of quotes my ContactId
List<Currency> opportunities = Utils.getFieldListFromMap(quotes, 'Custom_Total__c');

If I cannot describe the field data-type by the <string> parameter I'm passing here, then perhaps I can pass another <String> parameter declaring the expected data-type that should return in the List.

Any thoughts on this? Is it even possible? If not, what can be done, perhaps just dynamic sObjects but not fields?

Thanks in advance for any help I can get with this

EDIT: WORKING EXAMPLE

public static List<sObject> getListFromMap (Map<Id,List<sObject>> object_map, String obj_type){
     List<sObject> objects = new List<sObject>();
     Schema.SObjectType object_schema = Schema.getGlobalDescribe().get(obj_type);

     for(Id id : object_map.keyset()){
          for (SObject obj : object_map.get(id) ) {
               objects.add(object_schema.newSObject(obj.Id));
               // This gets the object type based on the objects Id
          }
     }
     return objects;
}

List<CampaignMember> members = Utils.getListFromMap(campaign_members, 'CampaignMember');
List<Quote> quote_list= Utils.getListFromMap(quotes, 'Quote');

This method currently gets my back a List of the object type I want back rather then just a generic sObect type. Now I just need to be able to do this with fields as well.

  • If you passed a Map<Id,List<sObject>> to a method, I'd expect an array of List<field-primitiveType> to be returned (one for each ID in the Map) which I don't think is what you want. I suspect you want to pass a List<sObject> and have a List<field-primitiveType> returned instead. – crmprogdev Jan 31 '15 at 2:06
  • 1
    As for the "working example", here's a tip: Use SObjectType and SObjectField instead. This avoids compile-time typos that only manifest at run-time. – sfdcfox Feb 2 '15 at 6:33
9

You don't need to have a return type to get data back to the callee, and sometimes this is more convenient. Here's my version of a "get values from records" implementation:

public static void getValuesFromRecords(Object[] result, SObject[] source, SObjectField field) {
    for(SObject record: source) {
        result.add(record.get(field));
    }
}

Typical usage:

Decimal[] totalPrices = new Decimal[0];
Utils.getValuesFromRecords(totalPrices, Trigger.new, Opportunity.Amount);

The result is that totalPrices is loaded with the values from the records. Best of all, no casts are ever required, and everything "just works."

This works because objects are passed by reference, as they are in Java, so by adding the values directly to the callee's array, type-casting is transparent, and largely automatic (but beware, using the wrong data type can result in errors, of course).

Of course, in my particular utility class, there were different permutations, such as assigning a single value to many fields, or assigning different values to many fields (via Map<SobjectField, Object>). Feel free to explore.

This particular pattern is common in languages like C and C++, but apparently rare in Java (because of the concept of encapsulation), and therefore also in Apex Code. However, this pattern is often the only way to achieve maximum code efficiency, and it would be worth a developer's time to learn this technique for times this might be necessary.


Edit: Based on some comments, I've provided a more utilitarian function that returns a value that you can use immediately:

public static Object[] getValuesFromField(Type listType, SObject[] records, SObjectField field) {
    Object[] results = (Object[])listType.newInstance();
    for(SObject record: records) {
        results.add(record.get(field));
    }
    return results;
}

This new version allows a developer to specify what the return type should be. This may cause additional casting to be required, but doesn't require an already existing array to be used. Here's the new version in use:

Decimal[] totalPrices = (Decimal[])Utils.getValuesFromField(Decimal[].class, Trigger.new, Opportunity.Amount);
  • Coming from PHP background this passed by reference is new and although I knew about it, it's a little mind blowing to realize it can work the way you described here. Can't wait to try this! – Xtremefaith Feb 2 '15 at 6:49
  • @Xtremefaith I've added a secondary means of passing an explicit list type that you can use as well. It eliminates the need to have a variable already defined to load values from. – sfdcfox Feb 2 '15 at 16:56
  • @sfdcfox - excellent!; a variant of this that returns sets is handy for getting lookup ids from a list of child sobjects when one must query for the parents. – cropredy Feb 2 '15 at 19:26
  • @sfdcfox Any reason this wouldn't work with Set? Currently trying to pass Set<Quote> quoteSet = new Set<Quote>(); to utility method structured like this: public static void methodName (Set<sObject> result){ and I'm getting an error: [ERROR] Method does not exist or incorrect signature: Utils.methodName(SET<Quote>). If I change this to List it works perfectly fine... :( – Xtremefaith Feb 4 '15 at 2:38
  • @sfdcfox I'm still having an issue with Sets that I'm not understanding. I duplicated the function only substituting Set<sObject> from List<sObject>. Every time I use it I get this error though: Method does not exist or incorrect signature: Utils.getListFromMapList(SET<Contact>, MAP<Id,LIST<Contact>>). If I pass something data-typed as Set<sObject> it will work but then the result is not object specific so I get issues still, even if I cast it later as the Object type that I need :( – Xtremefaith Feb 13 '15 at 2:19
10

It is absolutely possible! We have even built this exact utility where I work. However, I can't just give you the answer, but I will give some hints.

  • SObject methods: get will return the value of the field you are looking for
  • The aforementioned get method can take either a String or a Schema.SObjectField to specify the field you want to pluck. If you maintain that API your utility will be much nicer to use.
  • You probably want to return a Set of unique values and cast the type, which means one method (or collection thereof) per return type.
  • You may want to ignore nulls, especially if you want to use this utility in line in your SOQL.
  • If you want this utility to be able to traverse cross object relationships, it may be worth writing some sort of top level Field Reference class that does that sort of work for you. This will reap rewards in other utilities as well.

Here is a little bit of sample code to get you started.

public Set<Decimal> decimals(String field, List<SObject> records)
{
    Set<Decimal> results = new Set<Decimal>();
    for (SObject record : records)
    {
        Decimal result = Decimal.valueOf(record.get(field));
        if (result != null) results.add(result);
    }
    return results;
}
public Set<Decimal> decimals(String field, Map<Id, SObject> records)
{
    return decimals(field, records.values());
}
public Set<Decimal> decimals(Schema.SObjectField field, List<SObject> records)
{
    // Fill in the blanks!
}
public Set<Decimal> decimals(Schema.SObjectField field, Map<Id, SObject> records)
{
    // Fill in the blanks!
}

No more hints until you post more code!

  • So I have to write multiple options of the same utility to accept different combinations of types? In your example you have Set<Decimal> but I want a method that can return a List<any_field_type>, as you said perhaps I could cast it dynamically? I don't know how to make the return data-type that flexible. As you mentioned Set I would probably also then make a utility method like Utils.getFieldSetFromMap(Map<Id,sObject>) or Utils.getFieldSetFromList(List<sObject>). I will post an example of one I kinda have working right now. – Xtremefaith Jan 31 '15 at 20:13
  • You can always add a method to your Utility Class that returns a list from a set using addAll(list<sObject>). – crmprogdev Jan 31 '15 at 20:34
  • You may want to look at ApexUtils which is a free set of Utils available either as an unmanaged paged on the App Exchange of as source from code.google.com/p/apex-lang. I believe Andrew Fawcett has taken over caretaking of the repository for it on Git & don't have the link at hand. – crmprogdev Jan 31 '15 at 20:44
  • This approach is particularly verbose, and requires a lot more code coverage than a simpler solution. It certainly would work in a pinch, though. – sfdcfox Feb 2 '15 at 6:30
  • 2
    @sfdcfox I disagree that it is more verbose than the solution you propose, if only because OP is requesting a utility class. IMHO the utility should require as little verbosity as possible to USE, and the verbosity within the API is irrelevant. You only write your utility once, but use it many times over. In my example, you do not even need to declare or instantiate the result if you want to use it in, say, a query. – Adrian Larson Feb 2 '15 at 15:25
0

I think you're trying to reinvent the wheel. Pretty much everything you're looking for already exists in various classes of the Apex-lang Utils Open Source Project. Here's just one example. Its the SObjectUtils.cls.

/* ============================================================
 * This code is part of the "apex-lang" open source project avaiable at:
 * 
 *      http://code.google.com/p/apex-lang/
 *
 * This code is licensed under the Apache License, Version 2.0.  You may obtain a 
 * copy of the License at:
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * ============================================================
 */
global class SObjectUtils {


    //NOTE: would've preferred to use a Set instead of a List but unfortunately, Sets of Enums
    //  are not allowed as of Spring '10.  Seems silly they aren't.  Especially since it
    //  necessitates a custom "contains" method.  Even though using a List is a workaround, it
    //  still looks better than multiple ORs in the bottom of the copyFields method.
    private static final List<Schema.DisplayType> STRING_TYPES      = new List<Schema.DisplayType>{
        Schema.DisplayType.base64
        ,Schema.DisplayType.Email
        ,Schema.DisplayType.MultiPicklist
        ,Schema.DisplayType.Phone
        ,Schema.DisplayType.Picklist
        ,Schema.DisplayType.String
        ,Schema.DisplayType.TextArea
        ,Schema.DisplayType.URL
    };
    private static final List<Schema.DisplayType> INTEGER_TYPES     = new List<Schema.DisplayType>{
        Schema.DisplayType.Integer
    };
    private static final List<Schema.DisplayType> ID_TYPES          = new List<Schema.DisplayType>{
        Schema.DisplayType.ID
        ,Schema.DisplayType.Reference
    };
    private static final List<Schema.DisplayType> DOUBLE_TYPES      = new List<Schema.DisplayType>{
        Schema.DisplayType.Currency
        ,Schema.DisplayType.Double
        ,Schema.DisplayType.Percent
    };
    private static final List<Schema.DisplayType> DATETIME_TYPES    = new List<Schema.DisplayType>{
        Schema.DisplayType.DateTime
    };
    private static final List<Schema.DisplayType> DATE_TYPES        = new List<Schema.DisplayType>{
        Schema.DisplayType.Date
    };
    private static final List<Schema.DisplayType> BOOLEAN_TYPES     = new List<Schema.DisplayType>{
        Schema.DisplayType.Boolean
        ,Schema.DisplayType.Combobox
    };

    global static Boolean isAnyFieldBlank(SObject obj, String[] fields){
        return ArrayUtils.isNotEmpty(getBlankFields(obj,fields));
    }

    global static String[] getBlankFields(SObject obj, String[] fields){
        if(obj == null || ArrayUtils.isEmpty(fields)){
            return new String[]{};
        }
        List<String> blankFields = new List<String>();
        Object value = null;
        for(String field : fields){
            value = obj.get(field);
            if(value == null || (value instanceof String && StringUtils.isBlank((String)value))){
                blankFields.add(field);
            }
        }
        return blankFields;
    }

    /*
    private static final Map<String,Schema.DescribeSObjectResult> cachedDescribes = new Map<String,Schema.DescribeSObjectResult>();
    global static Schema.DescribeSObjectResult getCachedDescribe(SObject obj){
        if(obj == null){
            return null;
        }
        final String objectApiName = ''+ obj.getsObjectType();
        if(!cachedDescribes.containsKey(objectApiName)){
            cachedDescribes.put(objectApiName, obj.getsObjectType().getDescribe());
        }
        return cachedDescribes.get(objectApiName);
    }
    */

    global static String toString(SObject obj){
        if(Limits.getFieldsDescribes() >= Limits.getLimitFieldsDescribes()){
            return null;
        }
        if(obj == null){
            return 'null';
        }
        Schema.DescribeSObjectResult objDesc = obj.getSObjectType().getDescribe();
        List<String> fieldValues = new List<String>();
        Map<String, Schema.SObjectField> m = objDesc.fields.getMap();        
        for (Schema.SObjectField f : m.values()) { 
            Schema.DescribeFieldResult d = f.getDescribe();    
            fieldValues.add(d.getName() + '=' + obj.get(d.getName()));
        }
        return '<#' + objDesc.getName() + '(' + StringUtils.joinArray(fieldValues,',') + ')>';
    }

    /*
    //Commenting this out because it can't be reliably tested.  In order to get 100%
    //  code coverage on this method, an org MUST have at least one queue (the test
    //  case actually needs a Lead queue).  But since that can't be controlled by
    //  test code, I'm just commenting this out but it in the source in case anyone
    //  would like to re-use it in the future.  If you do want to re-use it, you'll
    //  need to do one of the following:
    //    (1) customize test method SObjectUtilsTest.testSendNotificationEmail()
    //    (2) create a Lead queue

    global static void sendNotificationEmail(SObject obj){
        if(obj == null || obj.id == null){
            throw new IllegalArgumentException();
        }

        final Schema.DescribeSObjectResult objDesc = obj.getSObjectType().getDescribe();
        SObject retrieved = null;
        try{
            retrieved = Database.query(
               'select id,name,ownerid from '+ objDesc.getName() 
               + ' where id = \'' + obj.id + '\' and owner.type = \'Queue\'');
        }catch(QueryException e){}
        if(retrieved == null){
            throw new IllegalArgumentException();
        }
        final Set<String> emailSet = new Set<String>();
        final ID ownerId = (ID) retrieved.get('ownerid');
        final List<GroupMember> members = [select UserOrGroupId, Group.Email from GroupMember where groupid = :ownerId];
        final Set<ID> userIds = new Set<ID>();
        if(members != null && members.size() > 0){
            for(GroupMember member : members){
                userIds.add(member.UserOrGroupId);
                if(StringUtils.isNotEmpty(member.Group.Email)){
                    emailSet.add(member.Group.Email);
                }
            }
        }
        final List<User> users = [select id,email from user where id in :userIds];
        if(users != null && users.size() > 0){
            for(User usr : users){
                if(StringUtils.isNotEmpty(usr.email)){
                    emailSet.add(usr.email);
                }
            }
        }
        final List<String> emailList = new List<String>();
        for(String email : emailSet){
            emailList.add(email);
        }
        EmailUtils.sendTextEmail(   emailList,
                                    objDesc.getName() + ' transferred to you.',
                                    objDesc.getName() + ' ' + retrieved.get('name') + ' has been assigned to you. Please click on the link below to view the record.\n\nhttps://login.salesforce.com/?startURL=%2F' + retrieved.get('id')
        );
    }
    */

    global static SObject copyFields(SObject source, SObject destination){
        if(source == null || destination == null){
            return destination;
        }
        final Map<String,Schema.SObjectField> sourceFields = source.getSObjectType().getDescribe().fields.getMap();
        final Map<String,Schema.SObjectField> destinationFields = destination.getSObjectType().getDescribe().fields.getMap();
        final List<Schema.DescribeFieldResult> commonFieldsToCopy = new List<Schema.DescribeFieldResult>();

        Schema.DescribeFieldResult sourceField = null;
        Schema.DescribeFieldResult destinationField = null;
        for(String fieldName : sourceFields.keySet()){ 
            sourceField = sourceFields.get(fieldName).getDescribe();
            if(destinationFields.get(fieldName) != null){
                destinationField = destinationFields.get(fieldName).getDescribe();
                if( !sourceField.isAccessible()
                    || !destinationField.isAccessible()
                    || destinationField.isAutoNumber()
                    || destinationField.isCalculated()
                    || (StringUtils.isBlank((String)destination.get('id')) && !destinationField.isCreateable()) 
                    || (StringUtils.isNotBlank((String)destination.get('id')) && !destinationField.isUpdateable())
                    || sourceField.getType() != destinationField.getType()){
                    //source or destination field either can't or shouldn't be read or written to    
                    continue;
                }
                if(contains(STRING_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(String)source.get(sourceField.getName()));
                } else if(contains(INTEGER_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(Integer)source.get(sourceField.getName()));
                } else if(contains(ID_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(ID)source.get(sourceField.getName()));
                } else if(contains(DOUBLE_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(Double)source.get(sourceField.getName()));
                } else if(contains(DATETIME_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(DateTime)source.get(sourceField.getName()));
                } else if(contains(DATE_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(Date)source.get(sourceField.getName()));
                } else if(contains(BOOLEAN_TYPES,sourceField.getType())){
                    destination.put(sourceField.getName(),(Boolean)source.get(sourceField.getName()));
                }
            }
        }
        return destination;
    }   

    private static Boolean contains(List<Schema.DisplayType> aListActingAsSet, Schema.DisplayType typeToCheck){
        if(aListActingAsSet != null && aListActingAsSet.size() > 0){
            for(Schema.DisplayType aType : aListActingAsSet){
                if(aType == typeToCheck){
                    return true;
                }
            }
        }
        return false;
    }

    global static Object putQuietly(SObject sobj, String fieldName, Object value){
        Object old = null;
        if(sobj != null && fieldName != null){
            try{
                old = getQuietly(sobj,fieldName);

                //NOTE:  the following line doesn't handle nulls for some strange reason
                //  in Spring '10.  In order to make it work, the if is needed to explicitly 
                //  pass null.
                //sobj.put(fieldName,value);
                if(value==null){
                    sobj.put(fieldName,null);
                }else{
                    sobj.put(fieldName,value);
                }
            }catch(System.SObjectException e){}
        }
        return old;
    }

    global static Object getQuietly(SObject sobj, String fieldName){
        Object returnValue = null;
        if(sobj != null && fieldName != null){
            try{
                returnValue = sobj.get(fieldName);
            }catch(SObjectException e){}
        }
        return returnValue;
    }


}
  • apex-lang is overly complicated most of the time, and overly protective. For example, here's how I'd write "getQuietly(SObject, String)": try { return sobj.get(fieldName); } catch(exception e) { return null; } Then again, I'd probably not try to suppress all messages anyways, because this only hides logic errors that are hard to find in a developer's code. – sfdcfox Feb 2 '15 at 6:29
  • From my view, its more a case that everything needed to write the utility class exactly he wants is already contained in the code that exists in apex-lang. One can learn a great deal from studying that code. – crmprogdev Feb 2 '15 at 14:23
0

Try R.apex(https://github.com/Click-to-Cloud/R.apex), and it provides tons of utility methods in a functional way. Code like this should work.

R.of(accountList).pluck('Description').toStringList()

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.