I will address both challenges here.
In order to circumvent the first "in-memory" issue and also prevent code modification with each field addition, we'll generate a dynamic list of fields for the sObjects in question, then run and store a SoQL query with the resulting fields. Another trick I've included is the ability to exclude specific fields for a given sObject. This may be important if there is a specific field that should be reset upon cloning (for instance, maybe you want to reset the "Stage" field when you clone an existing, completed Opportunity).
To address the children, grand-children, great-grand-children, etc I've employed two inner classes, which will serve as a storage medium while we iterate through and identify all levels of parents and children we wish to clone. These classes will also help house the relationship information for later use, such as the name of the child object and the name of the field which is used to tie it to the parent (an example here would be the OpportunityId field for the OpportunityLineItem table).
Without further ado, let's get to it!
1. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named ApexMessage with one field (ApexMessage):
2. Once created, click "Manage" for the ApexMessage Custom Setting and then click "New" to enter a new message:
**Feel free to re-word the message to your liking, but please note the Name must be exact.
3. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named ChildRelationship with two fields (ChildRelationship, sObjectType):
4. Once created, click "Manage" for the ChildRelationship Custom Setting and then click "New" to enter a new relationship:
**The ChildRelationship field contains the actual child relationship name from the lookup
5. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named FieldExclusion with two fields (FieldExclusion, sObjectType):
6. Once created, click "Manage" for the FieldExclusion Custom Setting and then click "New" to enter a new field exclusion:
**The FieldExclusion field contains the actual API name for the fields you wish to exclude
7. Create a new Custom Setting (Setup > Develop > Custom Settings > New) and create a setting named FieldDefault with three fields (FieldDefault, FieldValue, sObjectType):
8. Once created, click "Manage" for the FieldDefault Custom Setting and then click "New" to enter a new field default:
**The FieldDefault field contains the actual API name for the fields you wish to exclude
9. Create a new Button (Setup > Customize > Opportunities > Buttons, Links, and Actions > New Button or Link) and create a button:
**You may choose to create this button on any other table, that's the beauty of the tool!
10. Add the Button from step nine to your page layout and enjoy!
11. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:
global class UtilsCustomSetting { private static final Map<String, String> mapApexMessages = mapApexMessages(); private static final Map<String, Set<String>> mapChildRelationships = mapChildRelationships(); private static final Map<String, Set<String>> mapFieldExclusions = mapFieldExclusions(); private static final Map<String, Map<String, String>> mapFieldDefaults = mapFieldDefaults(); global static Map<String, String> mapApexMessages() { List<ApexMessage__c> msgs = ApexMessage__c.getAll().values(); Map<String, String> options = new Map<String, String>(); for (ApexMessage__c msg : msgs) { options.put(msg.Name, msg.ApexMessage__c); } return options; } global static Map<String, Set<String>> mapChildRelationships() { List<ChildRelationship__c> excls = ChildRelationship__c.getAll().values(); Map<String, Set<String>> mapOf = new Map<String, Set<String>>(); for (ChildRelationship__c excl : excls) { if (!mapOf.containsKey(excl.sObjectType__c)) { mapOf.put(excl.sObjectType__c, new Set<String>()); } mapOf.get(excl.sObjectType__c).add(excl.ChildRelationship__c); } return mapOf; } global static Map<String, Set<String>> mapFieldExclusions() { List<FieldExclusion__c> excls = FieldExclusion__c.getAll().values(); Map<String, Set<String>> mapOf = new Map<String, Set<String>>(); for (FieldExclusion__c excl : excls) { if (!mapOf.containsKey(excl.sObjectType__c)) { mapOf.put(excl.sObjectType__c, new Set<String>()); } mapOf.get(excl.sObjectType__c).add(excl.FieldExclusion__c.toLowerCase()); } return mapOf; } global static Map<String, Map<String, String>> mapFieldDefaults() { List<FieldDefault__c> defs = FieldDefault__c.getAll().values(); Map<String, Map<String, String>> mapOf = new Map<String, Map<String, String>>(); for (FieldDefault__c def : defs) { if (!mapOf.containsKey(def.sObjectType__c)) { mapOf.put(def.sObjectType__c, new Map<String, String>()); } mapOf.get(def.sObjectType__c).put(def.FieldDefault__c.toLowerCase(), def.FieldValue__c); } return mapOf; } //** Returns a message for use in APEX error handling, chatter posting, etc global static String getApexMessage(String msgName) { return mapApexMessages.get(msgName); } //** Returns a set of child relationships to include in the cloning query global static Set<String> getChildRelationships(String relName) { Set<String> setOf = new Set<String>(); if (mapChildRelationships.containsKey(relName)) { setOf.addAll(mapChildRelationships.get(relName)); } return setOf; } //** Returns a set of fields to exclude from the cloning query global static Set<String> getFieldExclusions(String esclName) { Set<String> setOf = new Set<String>(); if (mapFieldExclusions.containsKey(esclName)) { setOf.addAll(mapFieldExclusions.get(esclName)); } return setOf; } //** Returns a set of fields to exclude from the cloning query global static Set<String> getFieldDefaults(String defName) { Set<String> setOf = new Set<String>(); if (mapFieldDefaults.containsKey(defName)) { setOf.addAll(mapFieldDefaults.get(defName).keySet()); } return setOf; } //** Returns a set of fields to exclude from the cloning query global static String getFieldDefault(String defName, String defValue) { if (mapFieldDefaults.containsKey(defName)) { if (mapFieldDefaults.get(defName).containsKey(defValue)) { return mapFieldDefaults.get(defName).get(defValue); } } return null; } }
12. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:
global without sharing class UtilsVariables { global static Map<String, String> mapApexMessages() { List<ApexMessage__c> msgs = ApexMessage__c.getAll().values(); Map<String, String> options = new Map<String, String>(); for (ApexMessage__c msg : msgs) { options.put(msg.Name, msg.ApexMessage__c); } return options; } global static Map<String, Set<String>> mapChildRelationships() { List<ChildRelationship__c> excls = ChildRelationship__c.getAll().values(); Map<String, Set<String>> mapOf = new Map<String, Set<String>>(); for (ChildRelationship__c excl : excls) { if (!mapOf.containsKey(excl.sObjectType__c)) { mapOf.put(excl.sObjectType__c, new Set<String>()); } mapOf.get(excl.sObjectType__c).add(excl.ChildRelationship__c); } return mapOf; } global static Map<String, Set<String>> mapFieldExclusions() { List<FieldExclusion__c> excls = FieldExclusion__c.getAll().values(); Map<String, Set<String>> mapOf = new Map<String, Set<String>>(); for (FieldExclusion__c excl : excls) { if (!mapOf.containsKey(excl.sObjectType__c)) { mapOf.put(excl.sObjectType__c, new Set<String>()); } mapOf.get(excl.sObjectType__c).add(excl.FieldExclusion__c.toLowerCase()); } return mapOf; } global static Map<String, Map<String, String>> mapFieldDefaults() { List<FieldDefault__c> defs = FieldDefault__c.getAll().values(); Map<String, Map<String, String>> mapOf = new Map<String, Map<String, String>>(); for (FieldDefault__c def : defs) { if (!mapOf.containsKey(def.sObjectType__c)) { mapOf.put(def.sObjectType__c, new Map<String, String>()); } mapOf.get(def.sObjectType__c).put(def.FieldDefault__c.toLowerCase(), def.FieldValue__c); } return mapOf; } //** Maps, held privately so as to only pull once per call private static final Map<String, String> mapApexMessages = UtilsCustomSetting.mapApexMessages(); private static final Map<String, Set<String>> mapChildRelationships = UtilsCustomSetting.mapChildRelationships(); private static final Map<String, Set<String>> mapFieldExclusions = UtilsCustomSetting.mapFieldExclusions(); private static final Map<String, Map<String, String>> mapFieldDefaults = UtilsCustomSetting.mapFieldDefaults(); //** Returns a message for use in APEX error handling, chatter posting, etc global static String getApexMessage(String msgName) { return mapApexMessages.get(msgName); } //** Returns a set of child relationships to include in the cloning query global static Set<String> getChildRelationships(String relName) { Set<String> setOf = new Set<String>(); if (mapChildRelationships.containsKey(relName)) { setOf.addAll(mapChildRelationships.get(relName)); } return setOf; } //** Returns a set of fields to exclude from the cloning query global static Set<String> getFieldExclusions(String esclName) { Set<String> setOf = new Set<String>(); if (mapFieldExclusions.containsKey(esclName)) { setOf.addAll(mapFieldExclusions.get(esclName)); } return setOf; } //** Returns a set of fields to exclude from the cloning query global static Set<String> getFieldDefaults(String defName) { Set<String> setOf = new Set<String>(); if (mapFieldDefaults.containsKey(defName)) { setOf.addAll(mapFieldDefaults.get(defName).keySet()); } return setOf; } //** Returns a set of fields to exclude from the cloning query global static String getFieldDefault(String defName, String defValue) { if (mapFieldDefaults.containsKey(defName)) { if (mapFieldDefaults.get(defName).containsKey(defValue)) { return mapFieldDefaults.get(defName).get(defValue); } } return null; } }
13. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:
global class UtilsGeneral { global static sObject newBlankSObject(sObject so) { return so.getSObjectType().newSObject(); } }
14. Create a new Apex Class (Setup > Develop > Apex Classes > New) and paste the following code:
global with sharing class UtilsDeeperClone { //** Deeply clones a list of original records, and all related/requested children //** Original list must include the Id field global static List<Id> clone(List<sObject> originalObjects, Boolean chatterPost) { //** Method variables Map<Id, Id> mapIdOldNew = new Map<Id, Id>(); Map<String, List<Id>> mapObjectIds = new Map<String, List<Id>>(); Map<String, String> mapRelationshipFields = new Map<String, String>(); Map<String, Map<Id, sObject>> mapObjectQueries = new Map<String, Map<Id, sObject>>(); Map<Integer, Map<String, List<sObject>>> mapObject = new Map<Integer, Map<String, List<sObject>>>(); List<sObjectChildRelationship> myChildRelationships = new List<sObjectChildRelationship>(); //** Add Level 0 (originalObjects) Map<String, List<sObject>> mapObjectCurrent = new Map<String, List<sObject>>(); List<sObjectChildRelationshipRow> myChildRelationshipRows = new List<sObjectChildRelationshipRow>(); //** Process input sObjects by sObjectType Set<String> origObjectTypeNames = new Set<String>(); for (sObject originalObject : originalObjects) { if (!origObjectTypeNames.contains(originalObject.getSObjectType().getDescribe().getName())) { origObjectTypeNames.add(originalObject.getSObjectType().getDescribe().getName()); } } for (String origObjectTypeName : origObjectTypeNames) { for (sObject originalObject : originalObjects) { if (origObjectTypeName == originalObject.getSObjectType().getDescribe().getName()) { //** Add sObject to myChildRelationshipRows myChildRelationshipRows.add(new sObjectChildRelationshipRow(originalObject)); //** Populate Map<String, List<Id>> mapIdObjectTypeCurrent if (mapObjectCurrent.containsKey(origObjectTypeName)) { mapObjectCurrent.get(origObjectTypeName).add(originalObject); } else { mapObjectCurrent.put(origObjectTypeName, new List<sObject>{originalObject}); } } } //** Add myChildRelationshipRows to a myChildRelationships record with relationship information if (!myChildRelationshipRows.isEmpty()) { myChildRelationships.add(new sObjectChildRelationship(0, origObjectTypeName, null, myChildRelationshipRows)); } } //** Populate Map<Integer, Map<String, List<Id>>> mapObject mapObject.put(0, mapObjectCurrent); //** Loop through levels 1-n (children) for (Integer currentLevel = 1 ; currentLevel < 20 ; currentLevel++) { mapObjectCurrent = new Map<String, List<sObject>>(); if (mapObject.size() == currentLevel) { //** Loop through all tables for (String objType : mapObject.get(currentLevel-1).keySet()) { List<sObject> sObjectOriginals = mapObject.get(currentLevel-1).get(objType); //** Get complete list of all child relationships for the given table 'objType' List<Schema.ChildRelationship> childRelationships = sObjectOriginals.get(0).getSObjectType().getDescribe().getChildRelationships(); //** Exit loop once there are no more childRelationships if (!childRelationships.isEmpty()) { //** Loop through all child relationships for (Schema.ChildRelationship childRelationship : childRelationships) { //** Only run script when the child relationships is acceptable (Custom Setting) if (UtilsVariables.getChildRelationships(objType).contains( childRelationship.getRelationshipName())) { myChildRelationshipRows = new List<sObjectChildRelationshipRow>(); //** Name of the Child Table String childObjectTypeName = childRelationship.getChildSObject().getDescribe().getName(); //** Name of the Child Field which stores the Parent sObject Id String childObjectFieldName = childRelationship.getField().getDescribe().getName(); //** Build query to return all children in this relationship String childRelationshipQuery = 'SELECT ID'; childRelationshipQuery += ' FROM ' + childObjectTypeName; childRelationshipQuery += ' WHERE ' + childObjectFieldName; childRelationshipQuery += ' IN (\'' + sObjectOriginals.get(0).Id + '\''; for (Integer i = 1 ; i < sObjectOriginals.size() ; i++) { childRelationshipQuery += ', \'' + sObjectOriginals.get(i).Id + '\''; } childRelationshipQuery += ')'; //** Executive query to return all children in this relationship List<sObject> childRelationshipQueryResults = Database.query(childRelationshipQuery); //** Loop through all queried children for (sObject childObject : childRelationshipQueryResults) { //** Add sObject to myChildRelationshipRows myChildRelationshipRows.add(new sObjectChildRelationshipRow(childObject)); //** Populate Map<String, List<Id>> mapIdObjectTypeCurrent if (mapObjectCurrent.containsKey(childObjectTypeName)) { mapObjectCurrent.get(childObjectTypeName).add(childObject); } else { mapObjectCurrent.put(childObjectTypeName, new List<sObject>{childObject}); } } //** Add myChildRelationshipRows to a myChildRelationships record with relationship information if (!myChildRelationshipRows.isEmpty()) { myChildRelationships.add(new sObjectChildRelationship(currentLevel, childObjectTypeName, childObjectFieldName, myChildRelationshipRows)); } } } } } } //** Populate Map<Integer, Map<String, List<Id>>> mapObject if (!mapObjectCurrent.isEmpty()) { mapObject.put(currentLevel, mapObjectCurrent); } } //** Establish a list of Ids per each sObjectType for the result queries for (sObjectChildRelationship rel : myChildRelationships) { for (sObjectChildRelationshipRow row : rel.myChildRelationshipRowList) { if (!mapObjectIds.containsKey(rel.relationshipName)) { mapObjectIds.put(rel.relationshipName, new List<Id>()); } mapObjectIds.get(rel.relationshipName).add(row.sObjectOriginal.Id); if (!mapRelationshipFields.containsKey(rel.relationshipName)) { mapRelationshipFields.put(rel.relationshipName, rel.relationshipFieldName); } } } //** Loop through each sObjectType to query the results for use later for (String objName : mapObjectIds.keySet()) { //** List of all Ids for the records to be cloned List<Id> sObjectIds = new List<Id>(); sObjectIds = mapObjectIds.get(objName); //** List of all fields for the records to be cloned List<String> sObjectFields = new List<String>(); //** sObjectType Schema.SObjectType sObjectType = sObjectIds.get(0).getSObjectType(); //** Get all current fields from the object if (sObjectType != null) { Map<String, Schema.SObjectField> fieldMap = sObjectType.getDescribe().fields.getMap(); for (String fieldName : fieldMap.keySet()) { if (fieldMap.get(fieldName).getDescribe().isCreateable()) { sObjectFields.add(fieldName); } } } for (Integer i = 0; i < sObjectFields.size(); i++) { if (UtilsVariables.getFieldExclusions(sObjectType.getDescribe().getName()).contains(sObjectFields.get(i))) { sObjectFields.remove(i); } } //** If there are no records sent into the method, then return an empty list if (sObjectIds != null && !sObjectIds.isEmpty() && !sObjectFields.isEmpty()) { //** Construct a SOQL query to get all field values of all records (sort Id ascending) String sObjectFieldsQuery = 'SELECT ' + sObjectFields.get(0); for (Integer i = 1; i < sObjectFields.size(); i++) { sObjectFieldsQuery += ', ' + sObjectFields.get(i); } sObjectFieldsQuery += ' FROM ' + sObjectType.getDescribe().getName(); sObjectFieldsQuery += ' WHERE Id IN (\'' + sObjectIds.get(0) + '\''; for (Integer i = 1 ; i < sObjectIds.size() ; i++) { sObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\''; } sObjectFieldsQuery += ')'; System.debug('##### sObjectFieldsQuery: ' + sObjectFieldsQuery); List<sObject> sObjectFieldsQueryResults = Database.query(sObjectFieldsQuery); Map<Id, sObject> mapObjectFieldsQueryResults = new Map<Id, sObject>(); for (sObject obj : sObjectFieldsQueryResults) { mapObjectFieldsQueryResults.put(obj.Id, obj); } mapObjectQueries.put(objName, mapObjectFieldsQueryResults); } } //** Loop through each level to insert while adding the correct parent identification for (Integer currentLevel = 0 ; currentLevel < 20 ; currentLevel++) { List<sObject> sObjectsToClone = new List<sObject>(); List<Id> listIdOld = new List<Id>(); List<Id> listIdNew = new List<Id>(); for (sObjectChildRelationship cloneChildRelationship : myChildRelationships) { if (cloneChildRelationship.relationshipLevel == currentLevel) { sObjectsToClone = new List<sObject>(); for (sObjectChildRelationshipRow cloneChildRelationshipRow : cloneChildRelationship.myChildRelationshipRowList) { listIdOld.add(cloneChildRelationshipRow.sObjectOriginal.Id); sObject orig = mapObjectQueries.get(cloneChildRelationship.relationshipName).get( cloneChildRelationshipRow.sObjectOriginal.Id); sObject clone = UtilsGeneral.newBlankSObject(orig); Map<String, Schema.SObjectField> fieldMap = clone.getSObjectType().getDescribe().fields.getMap(); System.debug('##### Clone: Before = ' + clone); for (String fieldName : fieldMap.keySet()) { if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() && !UtilsVariables.getFieldExclusions(clone.getSObjectType().getDescribe().getName()).contains( fieldName)) { clone.put(fieldName, orig.get(fieldName)); System.debug('##### Clone: Field Clone = ' + fieldName + ': ' + orig.get(fieldName)); } if (fieldName != 'Id' && fieldMap.get(fieldName).getDescribe().isCreateable() && UtilsVariables.getFieldDefaults(clone.getSObjectType().getDescribe().getName()).contains( fieldName)) { clone.put(fieldName, UtilsVariables.getFieldDefault(clone.getSObjectType().getDescribe().getName(), fieldName)); System.debug('##### Clone: Field Default = ' + fieldName + ': ' + UtilsVariables.getFieldDefault( clone.getSObjectType().getDescribe().getName(), fieldName)); } } if (cloneChildRelationship.relationshipFieldName != null) { clone.put(cloneChildRelationship.relationshipFieldName, mapIdOldNew.get((Id) mapObjectQueries.get( cloneChildRelationship.relationshipName).get(cloneChildRelationshipRow.sObjectOriginal.Id).get( cloneChildRelationship.relationshipFieldName))); System.debug('##### Clone: Field Relationship = ' + cloneChildRelationship.relationshipFieldName + ': ' + mapIdOldNew.get((Id) mapObjectQueries.get(cloneChildRelationship.relationshipName).get( cloneChildRelationshipRow.sObjectOriginal.Id).get( cloneChildRelationship.relationshipFieldName))); } System.debug('##### Clone: After = ' + clone); sObject cloned = clone.clone(false, true); sObjectsToClone.add(cloned); System.debug('##### Clone: Cloned = ' + cloned); } //** Insert cloned records insert sObjectsToClone; //** Populate list with cloned (new) Ids [assumption is it's the same order as listIdOld] for (sObject newObject : sObjectsToClone) { listIdNew.add(newObject.Id); } //** Fail gracefully if listOldId.size() != listNewId.size() System.assertEquals(listIdNew.size(), listIdOld.size()); //** Map the original (old) Ids to the cloned (new) Ids for (Integer i = 0 ; i < listIdOld.size() ; i++) { mapIdOldNew.put(listIdOld.get(i), listIdNew.get(i)); if (chatterPost && currentLevel == 0) { UtilsChatter.postChatterFeedItem(ConnectApi.FeedType.Record, listIdNew.get(i), UtilsVariables.getAPEXMessage('CloneChatterMessage') + listIdOld.get(i)); } } } } } List<Id> clonedObjectIds = new List<Id>(); for (sObject originalObject : originalObjects) { clonedObjectIds.add(mapIdOldNew.get(originalObject.Id)); } return clonedObjectIds; } //** Allow pages and external services to trigger a clone from a single object with or without a Chatter post webService static Id clone(Id originalId, Boolean chatterPost) { //** Build query to return the full object from an Id String originalObjectQuery = 'SELECT ID'; originalObjectQuery += ' FROM ' + originalId.getSObjectType().getDescribe().getName(); originalObjectQuery += ' WHERE Id = \'' + originalId + '\''; //** Executive query to return the full object from an Id List<sObject> originalObject = Database.query(originalObjectQuery); //** Execute clone() List<Id> clonedIds = clone(originalObject, chatterPost); return clonedIds[0]; } //** Houses the relationship information with the list of related records public class sObjectChildRelationship { //** What level of child is it? 0=Parent 1=Child 2=Grand-Child 3=Great-Grand-Child etc... public Integer relationshipLevel {get; set;} //** What table are the related records stored in? public String relationshipName {get; set;} //** What is the field name that stores the parent-child relationship? public String relationshipFieldName {get; set;} //** Houses the list of actual records public List<sObjectChildRelationshipRow> myChildRelationshipRowList {get; set;} //** Initialize public sObjectChildRelationship(Integer relationshipLevel, String relationshipName, String relationshipFieldName, List<sObjectChildRelationshipRow> myChildRelationshipRowList) { this.relationshipLevel = relationshipLevel; this.relationshipName = relationshipName; this.relationshipFieldName = relationshipFieldName; this.myChildRelationshipRowList = myChildRelationshipRowList; } } //** Houses the actual record public class sObjectChildRelationshipRow { //** What record am I cloning? public sObject sObjectOriginal {get; set;} //** Initialize public sObjectChildRelationshipRow(sObject sObjectOriginal) { this.sObjectOriginal = sObjectOriginal; } } }
A few notes:
- In order to clone master detail relationships, the option to re-parent must be selected on the field setup
- You can get the utilChatter class here: utilChatter
- If Opportunity is one of your child objects, you need to add these field exclusions at minimum: Amount
- If OpportunityLineItem is one of your child objects, you need to add these field exclusions at minimum: isDeleted,SortOrder,TotalPrice,ListPrice,Subtotal,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModStamp
Happy Coding!