Monday, July 22, 2019

Salesforce - Generate dynamic inner query to fetch parent and related child records

Use Case: In many scenarios, we need to clone the records with related child records.

Issue / Limitation: We may simply use the "clone" method from SObject class. But if we use the clone method, we will get only fields available in the user interface (pagelayout)

Solution: Using inner query we can fetch the records with all fields

Note: SOQL will support only 20K characters, so better to include only necessary fields (not all the fields from schema) in the queries

Below is the example code to query Account and it's related Contacts and Opportunity records.

DataService Class
/**
* @author     Rajadurai Kalyanasundaram
* @date       July 23, 2019
* @decription Data Service class to query the parent and child records
*/
public class DataService{

    public static Map<String, String> childObjAPIAndRelName = new Map<String, String>();
    public static Set<String> parentObjRelFields = new Set<String>();
    public static Map<String, String> childConditions = new Map<String, String>();
    public static Map<String, String> objectAndListOfFields = new Map<String, String>();
 
     /**
    * @method getAccountAndChildRecordsData
    * @param List<Id> 
    * @return  List<Account>
    * Description: Pass list of parent object Id's. It will return the parent record with its child records
    */
    public static List<Account> getAccountAndChildRecordsData(List<Id> parentRecordIdSet){
     
        //Just add the child object API name and its relationship name
        childObjAPIAndRelName = new Map<String, String>{
                'Opportunity' => 'Opportunities',
                'Contact' => 'Contacts'
        };
     
        //Here you can pass any filter criteria for child objects
        childConditions = new Map<String, String>{
            'Opportunity' => ' WHERE AccountId IN: parentRecordIdSet ',
            'Contact' => ' WHERE AccountId IN: parentRecordIdSet '
        };
     
        //Optionally you can pass the parent object relationship fields
        parentObjRelFields = new Set<String>();
     
        //In this map, you can pass the child object fields. If you are not passing the child object fields, it will take all the fields from schema
        objectAndListOfFields = new Map<String, String>{             
            'Contact' => 'Id, Name, FirstName, LastName, Birthdate, Email, AccountId',
            'Opportunity' => 'Id, Name, AccountId, Amount'
        };

        //getInnerQuery-ParentObject, RelationshipFields, childObjAPIAndRelName, contactIdSet, parentQueryFilter
        String queryString = Utility.getInnerQuery('Account', objectAndListOfFields, parentObjRelFields, childObjAPIAndRelName, childConditions, parentRecordIdSet, 'ORDER BY Name');
     
        List<Account> accList = Database.query(queryString);
        for(Account acc: accList){
         
            System.debug('Parent Account::::'+acc.Name);
            for(Contact con: acc.Contacts){
             
                System.debug('Child Contact::::'+con.Name);
            }
         
            for(Opportunity opp: acc.Opportunities){
             
                System.debug('Child Opportunity::::'+opp.Id);
            }
        }
        return accList;
    }

}

Utility Class
/**
* @author     Rajadurai Kalyanasundaram
* @date       July 23, 2019
* @decription Utility class to generate the dynamic inner query
*/
public class Utility {
 
    /**
    * @method getInnerQuery
    * @param1 ParentObjectName
    * @param2 parentObjectRelationshipfields
    * @param3 Map of child objects API name and relationship name
    * @param4 Set of Id's
    * @param5 Query condition for parent query
    * @description Method  to build the dynamic inner query
    * @return  Query String
    */
    public static String getInnerQuery(String parentObjectAPIName, Map<String, String> objectAndFields, Set<String> parentObjectRelationshipfields, Map<String, String> childObjAPIAndRelName, Map<String, String> childObjectConditions, List<Id> parentRecordIdSet, String parentQueryCondition) {
     
     
        String parentObjFieldsString = '';
        if ( parentObjectRelationshipfields == NULL ) parentObjectRelationshipfields = new Set<String> ();
     
        if(objectAndFields != null && objectAndFields.containsKey(parentObjectAPIName)){
         
            parentObjFieldsString = objectAndFields.get(parentObjectAPIName);
        }else{
         
            parentObjFieldsString = getObjectFields(parentObjectAPIName);
        }
     
        if(parentObjectRelationshipfields != null && parentObjectRelationshipfields.size() > 0){
         
            parentObjFieldsString += ', '+( String.join ( new List<String> ( parentObjectRelationshipfields), ',' + ' ' ) );
        }
     
        List<String> innerQueryStringList = new List<String>();
        String combinedInnerQueryString = '';
        for(String childObjectAPIName : childObjAPIAndRelName.keySet()){
         
            String condition = (childObjectConditions != null && childObjectConditions.containsKey(childObjectAPIName)) ? childObjectConditions.get(childObjectAPIName): '';
            String objectFieldsString = '';         
            if(objectAndFields != null && objectAndFields.containsKey(childObjectAPIName)){
             
                objectFieldsString = objectAndFields.get(childObjectAPIName);
            }         
         
            innerQueryStringList.add('('+getQuery(childObjectAPIName, objectFieldsString, condition, childObjAPIAndRelName.get(childObjectAPIName))+')');
        }
     
        combinedInnerQueryString = ( String.join ( new List<String> (innerQueryStringList), ',' + ' ' ) );
        String queryValue = 'SELECT ' + parentObjFieldsString + ','+ combinedInnerQueryString + ' FROM '+ parentObjectAPIName +' WHERE Id IN:parentRecordIdSet ';
     
        if(String.isNotBlank(parentQueryCondition)){
         
            // In the parent query, if we have only Order By we don't need to add 'AND' operator       
            if(parentQueryCondition.startsWithIgnoreCase('Order')){
             
                queryValue += ' '+parentQueryCondition;
            }else{
                queryValue += ' AND '+parentQueryCondition;
            }
        }     
        return queryValue;
    } 
 
    /**
    * @method getQuery
    * @param1 Object name
    * @param2 Set of relationship fields
    * @param3 Query condition
    * @param4 Child object relationship name
    * @description Method  to build the dynamic query
    * @return  Query String
    */
    public static String getQuery(String objectName, String fieldString, String condition, String relationshipName) {
     
        String objFieldsString = '';
        if ( String.isNotBlank(fieldString) ){
         
            objFieldsString = fieldString;
        }else{
         
            objFieldsString = getObjectFields(objectName);
        }
     
        List<String> strings = new List<String> ();
        strings.add ( 'SELECT ' );
        strings.add ( objFieldsString );
        strings.add ( 'FROM' );
        // relationshipName -> To build the inner query
        // objectName -> To build direct/parent object query
        if(String.isNotBlank(relationshipName)){
         
            strings.add (relationshipName);
        }else{
         
            strings.add (objectName);
        }
     
        if (!String.IsBlank ( condition ) ) {
         
            strings.add ( condition );
        }
        return String.join (strings, ' ' );
    }
 
    public static String getObjectFields(String objectName){
     
        // Get a map of field name and field token
        Map<String, Schema.SObjectField> items = Schema.getGlobalDescribe().get(objectName).getDescribe().Fields.getMap();
        List<String> selectFields = new list<String>();
     
        if (items != null){
         
            for (Schema.SObjectField ft : items.values()){ // loop through all field tokens (ft)
             
                Schema.DescribeFieldResult fd = ft.getDescribe(); // describe each field (fd)
                if (fd.isCreateable()){
                 
                    selectFields.add(fd.getName());
                }
            }
        }     
        return ( String.join ( new List<String> ( selectFields), ',' + ' ' ) );
    }
}

Code Snippet for testing 
Execute below sample code in developer console and check the output

System.debug(DataService.getAccountAndChildRecordsData( new List<Id>{'0017F00001vCyrq'}));

2 comments:

  1. Hi Durai,

    Good effort! Appreciate great work.

    One more suggestion, It looks like this is specific to Account and related object and why you should't make as more generic? that way it works across all object.

    If suppose give parent object Name it should take all associated child and and copy to the new cloned object.

    ReplyDelete
    Replies
    1. Hi Tamil,

      Thank you very much for your feedback. Good idea to make it generic for all objects. Will try to update in further post.

      Delete

Salesforce - Generate dynamic inner query to fetch parent and related child records

Use Case: In many scenarios, we need to clone the records with related child records. Issue / Limitation: We may simply use the "cl...