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'}));
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'}));
Hi Durai,
ReplyDeleteGood 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.
Hi Tamil,
DeleteThank you very much for your feedback. Good idea to make it generic for all objects. Will try to update in further post.