Last active
September 7, 2017 07:05
-
-
Save swapnilshrikhande/bca7a904a0907657a519aac194b8e58f to your computer and use it in GitHub Desktop.
Generic SObject Clone Utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public with sharing class SObjectClone { | |
public final Map<String, Id> recordTypeMap; | |
public Map<String,Map<String,String>> relationshipDetailMap; | |
public Schema.SObjectType objectType; | |
public Schema.FieldSet fieldSet; | |
public Set<String> relationshipNameSet; | |
public CloneInitializer initializer; | |
public Id cloneRecordTypeId; | |
public static final String FILTER_CLAUSE = ' WHERE ID IN :recordIdList'; | |
public CloneConfig config; | |
public class CloneConfig { | |
//clone config | |
public Boolean preserveId; | |
public Boolean isDeepClone; | |
public Boolean preserveReadonlyTimestamps; | |
public Boolean preserveAutonumber; | |
//default values | |
public CloneConfig(){ | |
this.preserveId = false; | |
this.isDeepClone = true; | |
this.preserveReadonlyTimestamps = false; | |
this.preserveAutonumber = false; | |
} | |
} | |
//initialize cloner for a particular type | |
public SObjectClone(Schema.SObjectType targetObject) { | |
recordTypeMap = Utils.getRecordTypeMapForObjectGeneric(targetObject); | |
relationshipDetailMap = Utils.getRelationshipDetails(targetObject); | |
objectType=targetObject; | |
} | |
public SObjectClone setCloneConfig(CloneConfig config){ | |
this.config = config; | |
return this; | |
} | |
public SObjectClone setFieldSet(Schema.FieldSet fieldSet){ | |
this.fieldSet = fieldSet; | |
return this; | |
} | |
public SObjectClone setRelationshipNameSet(Set<String> relationshipNameSet){ | |
this.relationshipNameSet = relationshipNameSet; | |
return this; | |
} | |
public SObjectClone setInitializer(CloneInitializer initializer){ | |
this.initializer = initializer; | |
return this; | |
} | |
public SObjectClone setTargetRecordTypeId(String targetRecordTypeDevName){ | |
this.cloneRecordTypeId = recordTypeMap.get(targetRecordTypeDevName); | |
return this; | |
} | |
//clone SObject base on source record id and fieldset | |
// context : New SObject is initialized based on the context passed, | |
// which specifies the buisness process the cloning is used in current context. | |
public SObject clone(Id recordId){ | |
Map<Id,SObject> clonedMap = cloneSObjectList(new Id[]{recordId}); | |
return clonedMap.get(recordId); | |
} | |
public Map<Id,SObject> cloneSObjectList(List<Id> recordIdList){ | |
//sanity checks, allow injecting config variable from external context | |
config = config==null ? new CloneConfig() : config; | |
//source id verses cloned SObject map | |
Map<Id,SObject> clonedSObjectMap = new Map<Id,SObject>(); | |
//generate select for the SObject and passed fieldset | |
String query = Utils.generateQuery(objectType, | |
fieldSet, | |
relationshipNameSet, | |
FILTER_CLAUSE ); | |
Map<Id,SObject> sourceSObjectMap = new Map<Id,SObject>( (List<SObject>)Database.query(query) ); | |
for ( SObject SObjectRecord : sourceSObjectMap.values() ){ | |
clonedSObjectMap.put( SObjectRecord.Id, | |
SObjectRecord.clone(config.preserveId, | |
config.isDeepClone, | |
config.preserveReadonlyTimestamps, | |
config.preserveAutonumber)); | |
} | |
if( initializer != null ){ | |
for(Id parentSObjectId : clonedSObjectMap.keySet()){ | |
SObject clonedSObject = clonedSObjectMap.get(parentSObjectId); | |
system.debug('>>> cloneRecordTypeId : '+cloneRecordTypeId); | |
clonedSObject.put('RecordTypeId',cloneRecordTypeId); | |
clonedSObject = initializer.initialize(parentSObjectId, clonedSObject); | |
} | |
} | |
if( false == clonedSObjectMap.isEmpty() ){ | |
insert clonedSObjectMap.values(); | |
} | |
List<SObject> clonedRelatedRecordList = new List<SObject>(); | |
//clone related records | |
SObject sourceSObject; | |
SObject clonedSObject; | |
Map<Integer,List<SObject>> dataToInsertMap = new Map<Integer,List<SObject>>(); | |
String fieldName; | |
Integer relationShipCount=0; | |
Integer bucketNumber =0; | |
Map<String,String> relationFieldDetailsMap; | |
for(Id parentId : recordIdList ){ | |
sourceSObject = sourceSObjectMap.get(parentId); | |
clonedSObject = clonedSObjectMap.get(parentId); | |
relationShipCount = 0; | |
for(String relationName : relationshipNameSet ){ | |
if( relationName == null ) | |
continue; | |
relationFieldDetailsMap = relationshipDetailMap.get(relationName.toLowerCase()); | |
if( relationShipDetailMap == null ) | |
continue; | |
//this code is required to make sure single bucket has records of at max 10 different relationships | |
//only, as salesforce does not allow dml insert on generic list containing data from more than 10 different objects | |
++relationShipCount; | |
bucketNumber = Integer.valueOf(relationShipCount/10); | |
if(dataToInsertMap.get(bucketNumber)==null){ | |
dataToInsertMap.put(bucketNumber,new List<SObject>()); | |
} | |
clonedRelatedRecordList = sourceSObject.getsObjects(relationName); | |
if( null != clonedRelatedRecordList ){ | |
for(SObject clonedRecord : clonedRelatedRecordList.deepClone() ){ | |
fieldName = relationFieldDetailsMap.get('RELATIONSHIP_FIELD'); | |
clonedRecord.put(fieldName,clonedSObject.Id); | |
dataToInsertMap.get(bucketNumber).add(clonedRecord); | |
} | |
} | |
} | |
} | |
//insert related records | |
for( Integer key : dataToInsertMap.keySet() ){ | |
if( null != dataToInsertMap.get(key) && false == dataToInsertMap.get(key).isEmpty() ){ | |
insert dataToInsertMap.get(key); | |
} | |
} | |
return clonedSObjectMap; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@isTest | |
private class SObjectCloneTest { | |
// method to setup test data for the test class | |
@testSetup static void setupTestData() { | |
// account | |
Account accountObj = new Account(); | |
accountObj = FRNOrgTestDataFactory.createInsComapnyAccountRecord(accountObj); | |
insert accountObj; | |
system.assertNotEquals(accountObj.Id, null); | |
//facility programs | |
Facility_Program__c fcltyPrgrm = new Facility_Program__c(); | |
fcltyPrgrm = FRNOrgTestDataFactory.createFcltyPrgrmRecord(fcltyPrgrm,1); | |
fcltyPrgrm.FRN_Facility__c = accountObj.Id; | |
insert fcltyPrgrm; | |
system.assertNotEquals(fcltyPrgrm.Id, null); | |
} | |
// method to test SObjectClone() | |
static testMethod void SObjectCloneTest() { | |
Test.startTest(); | |
SObjectClone cloneCls = new SObjectClone(Account.sObjectType); | |
Test.stopTest(); | |
System.AssertEquals(cloneCls.objectType, Account.sObjectType); | |
} | |
// method to test SObjectClone() | |
static testMethod void setFieldSetTest() { | |
Test.startTest(); | |
SObjectClone cloneCls = new SObjectClone(Account.sObjectType) | |
.setFieldSet(SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone); | |
Test.stopTest(); | |
//System.AssertEquals(cloneCls.fieldSet, SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone); | |
} | |
// method to test SObjectClone() | |
static testMethod void setTargetRecordTypeIdTest() { | |
Test.startTest(); | |
SObjectClone cloneCls = new SObjectClone(Account.sObjectType) | |
.setTargetRecordTypeId('Insurance_Company'); | |
Test.stopTest(); | |
//System.AssertEquals(cloneCls.fieldSet, SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone); | |
} | |
// method to test SObjectClone() | |
static testMethod void setCloneConfigTest() { | |
Test.startTest(); | |
//SObjectClone cloneCls = new SObjectClone(Account.sObjectType); | |
SObjectClone.CloneConfig configCls = new SObjectClone.CloneConfig(); | |
configCls.preserveId = true; | |
configCls.isDeepClone = true; | |
configCls.preserveReadonlyTimestamps = true; | |
configCls.preserveAutonumber = true; | |
SObjectClone cloneCls = new SObjectClone(Account.sObjectType); | |
cloneCls = cloneCls.setCloneConfig(configCls); | |
Test.stopTest(); | |
System.AssertEquals(cloneCls.config.preserveId, true); | |
System.AssertEquals(cloneCls.config.isDeepClone, true); | |
System.AssertEquals(cloneCls.config.preserveReadonlyTimestamps, true); | |
System.AssertEquals(cloneCls.config.preserveAutonumber, true); | |
} | |
// method to test setRelationshipNameSet() | |
static testMethod void setRelationshipNameSetTest() { | |
Test.startTest(); | |
Set<String> relationShipSet = new Set<String>{'Facility_Programs__r'}; | |
SObjectClone cloneCls = new SObjectClone(Account.sObjectType) | |
.setRelationshipNameSet(relationShipSet); | |
Test.stopTest(); | |
System.AssertEquals(cloneCls.relationshipNameSet.size(), relationShipSet.size()); | |
} | |
// method to test SObjectClone() | |
static testMethod void cloneTest() { | |
Test.startTest(); | |
Account accountObj = [SELECT Id, | |
(SELECT id | |
FROM Facility_Programs__r) | |
FROM Account | |
LIMIT 1]; | |
Set<String> relationShipSet = new Set<String>{'Facility_Programs__r'}; | |
InitializerTestCls initCls = new InitializerTestCls(); | |
initCls.accountName = 'Clone Insurance Company'; | |
SObjectClone.CloneConfig configCls = new SObjectClone.CloneConfig(); | |
configCls.preserveId = false; | |
configCls.isDeepClone = true; | |
configCls.preserveReadonlyTimestamps = true; | |
configCls.preserveAutonumber = true; | |
Account cloneAccount = (Account)new SObjectClone(Account.sObjectType) | |
.setInitializer(initCls) | |
.setRelationshipNameSet(relationShipSet) | |
.setCloneConfig(configCls) | |
.setFieldSet(SObjectType.Account.FieldSets.Clone_Insurance_Fields_to_Clone) | |
.setTargetRecordTypeId('Insurance_Company') | |
.clone(accountObj.Id); | |
Test.stopTest(); | |
Account accountToAssert = [SELECT id, | |
Name, | |
RecordTypeId, | |
(SELECT Id | |
FROM Facility_Programs__r) | |
FROM Account | |
WHERE Name = 'Clone Insurance Company']; | |
System.assertNotEquals(accountToAssert, null); | |
System.assertEquals(accountToAssert.Facility_Programs__r.Size(), accountObj.Facility_Programs__r.Size()); | |
} | |
// cloned account initializer | |
public class InitializerTestCls implements CloneInitializer{ | |
public String accountName = ''; | |
public SObject initialize(Id parentId, SObject clonedRecord){ | |
Account clonedAccount = (Account)clonedRecord; | |
clonedAccount.Name = accountName; | |
return clonedAccount; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Class which contains useful utility methods required across classes | |
public class Utils { | |
public static final String OBJECTNAME = 'RELATIONSHIP_OBJECT'; | |
public static final String FIELDNAME = 'RELATIONSHIP_FIELD'; | |
public static final String QUERY_TEMPLATE = 'SELECT {0} FROM {1} {2}'; | |
public static final String NESTED_QUERY = '({0})'; | |
public static final String COMMA_SEPARATOR = ', '; | |
public static final String FIELD_ID = 'Id'; | |
//cache holders | |
public static Map<String, Schema.SObjectType> GLOBALDESCRIBE; | |
//Sobject wise relationships details | |
public static Map<String, Map<String,Map<String,String>> > objectWiseRelationshipMap; | |
//cache methods | |
public static Schema.SObjectType getSObjectType(String objectName){ | |
//lazy load | |
if(null == GLOBALDESCRIBE){ | |
GLOBALDESCRIBE = Schema.getGlobalDescribe(); | |
} | |
return GLOBALDESCRIBE.get(objectName); | |
} | |
//static initializers | |
static { | |
objectWiseRelationshipMap = new Map<String, Map<String,Map<String,String>> >(); | |
} | |
//Util methods start | |
//Gerate query for all fields in the fieldset and all fields from the related objects | |
//relatedRelationshipNames is a map of | |
// Relationshipname => Related ObjectName | |
public static String generateQuery( Schema.sObjectType fromObject | |
, Schema.FieldSet fieldSet | |
, Set<String> relationshipNameSet | |
, String whereClause | |
){ | |
List<String> queryFieldList; | |
if( fieldSet != null ){ | |
queryFieldList = getFieldListFor(fieldSet); | |
} else { | |
queryFieldList = getFieldListFor(fromObject); | |
} | |
//generate nestedQueryClauses | |
if( null != relationshipNameSet ){ | |
queryFieldList.addAll( getInnerQueryList(fromObject,relationshipNameSet) ); | |
} | |
String query = generateQuery(''+fromObject, queryFieldList, whereClause); | |
return query; | |
} | |
public static List<String> getInnerQueryList(Schema.sObjectType fromObject,Set<String> relationshipNameSet){ | |
List<String> queryFieldList = new List<String>(); | |
String objectName; | |
List<String> relatedFieldList; | |
String relatedQuery; | |
Map<String,Map<String,String>> relationDetailsMap = getRelationshipDetails(fromObject); | |
System.debug('relationDetailsMap='+relationDetailsMap.keySet()); | |
Map<String,String> relationshipDetailMap; | |
for( String relationshipName : relationshipNameSet ){ | |
relationshipName = relationshipName!=null ? relationshipName.toLowerCase() : null; | |
relationshipDetailMap = relationDetailsMap.get(relationshipName); | |
if( relationshipDetailMap == null ){ | |
System.debug('relationshipDetailMap is null for '+relationshipName); | |
continue; | |
} | |
objectName = relationshipDetailMap.get('RELATIONSHIP_OBJECT'); | |
relatedFieldList = getFieldListFor(getSObjectType(objectName)); | |
relatedQuery = generateQuery( relationshipName | |
, relatedFieldList | |
, ''); | |
relatedQuery = String.format(NESTED_QUERY,new String[]{relatedQuery}); | |
queryFieldList.add(relatedQuery); | |
} | |
return queryFieldList; | |
} | |
//generate query for fields within a field set | |
public static String generateQuery( Schema.sObjectType fromObject | |
, Schema.FieldSet fieldSet | |
, String whereClause){ | |
return generateQuery(''+fromObject, getFieldListFor(fieldSet), whereClause); | |
} | |
//generate query for fieldnames passed in | |
public static String generateQuery( String fromObject | |
, List<String> fieldNameSet | |
, String whereClause){ | |
//sanity check | |
whereClause = whereClause == null ? '' : whereClause; | |
//local variables | |
String queryFieldsClause = ''; | |
for(String fieldName : fieldNameSet) { | |
//if not first time here, add a , | |
if(false == String.isBlank(queryFieldsClause)){ | |
queryFieldsClause += COMMA_SEPARATOR; | |
} | |
queryFieldsClause += fieldName; | |
} | |
//Corner cases if no fields in the field set field set | |
//1 | |
if( true == String.isBlank(queryFieldsClause) ){ | |
queryFieldsClause += FIELD_ID; | |
} | |
String query = String.format(QUERY_TEMPLATE, new String[]{ | |
queryFieldsClause | |
, ''+fromObject | |
, whereClause | |
}); | |
return query; | |
} | |
//Supporting methods | |
public static List<String> getFieldListFor(Schema.FieldSet fieldSet){ | |
List<String> fieldNameSet = new List<String>(); | |
for(Schema.FieldSetMember fieldSetMember : fieldSet.getFields()) { | |
fieldNameSet.add(fieldSetMember.getFieldPath()); | |
} | |
return fieldNameSet; | |
} | |
public static List<String> getFieldListFor(Schema.sObjectType fromObject){ | |
system.debug('fromObject='+fromObject); | |
List<String> fieldNameSet = new List<String>(); | |
Schema.DescribeFieldResult dfr; | |
for(Schema.SObjectField fieldInstance : fromObject.getDescribe().fields.getMap().values()) { | |
dfr = fieldInstance.getDescribe(); | |
//@TODO TO remove as all fields should be cloned | |
//if( dfr.isAccessible() ){ | |
fieldNameSet.add( dfr.getname() ); | |
//} | |
} | |
return fieldNameSet; | |
} | |
public static Boolean startsWith(String sourceString, String character){ | |
//sanity | |
if( sourceString == null ){ | |
return false; | |
} | |
return sourceString.trim().startsWith(character); | |
} | |
public static Map<String,Map<String,String>> getRelationshipDetails(Schema.SObjectType parentSobjectType){ | |
Map<String,Map<String,String>> relationDetailsMap = objectWiseRelationshipMap.get(''+parentSobjectType); | |
//if in cache return immediately | |
if( null != relationDetailsMap ){ | |
return relationDetailsMap; | |
} else { | |
relationDetailsMap = new Map<String,Map<String,String>>(); | |
} | |
Schema.DescribeSObjectResult describeResult = parentSobjectType.getDescribe(); | |
String fieldName; | |
Map<String,String> detailsMap; | |
String childRelationshipName; | |
for(Schema.ChildRelationship childRelationship : describeResult.getChildRelationships()){ | |
childRelationshipName = childRelationship.getRelationshipName(); | |
if( childRelationshipName == null ) | |
continue; | |
//objectname | |
detailsMap = new Map<String,String>(); | |
detailsMap.put(Utils.OBJECTNAME, ''+childRelationship.getChildSObject()); | |
//field | |
fieldName = childRelationship.getField().getDescribe().getName(); | |
detailsMap.put(Utils.FIELDNAME, fieldName); | |
childRelationshipName = childRelationshipName.toLowerCase(); | |
relationDetailsMap.put( childRelationshipName, detailsMap ); | |
} | |
objectWiseRelationshipMap.put(''+parentSobjectType,relationDetailsMap); | |
return relationDetailsMap; | |
} | |
//record type details utility methods | |
//Record types cache | |
private static Map<Schema.SObjectType,Map<String,Id>> rtypesCache; | |
private static List<sObject> results; | |
static { | |
rtypesCache = new Map<Schema.SObjectType,Map<String,Id>>();//convenient map, formatted from results. | |
results = new List<sObject>();//contains all recordtypes retrieved via SOQL | |
} | |
// Returns a map of active, user-available RecordType IDs for a given SObjectType, | |
// keyed by each RecordType's unique, unchanging DeveloperName | |
public static Map<String, Id> getRecordTypeMapForObjectGeneric(Schema.SObjectType token) { | |
// Do we already have a result? | |
Map<String, Id> mapRecordTypes = rtypesCache.get(token); | |
// If not, build a map of RecordTypeIds keyed by DeveloperName | |
if (mapRecordTypes == null) { | |
mapRecordTypes = new Map<String, Id>(); | |
rtypesCache.put(token,mapRecordTypes); | |
} else { | |
// If we do, return our cached result immediately! | |
return mapRecordTypes; | |
} | |
// Get the Describe Result | |
Schema.DescribeSObjectResult obj = token.getDescribe(); | |
//Check if we already queried all recordtypes. | |
if (results == null || results.isEmpty()) { | |
// Obtain ALL Active Record Types | |
// (We will filter out the Record Types that are unavailable | |
// to the Running User using Schema information) | |
String soql = 'SELECT Id, Name, DeveloperName, sObjectType FROM RecordType WHERE IsActive = TRUE'; | |
try { | |
results = Database.query(soql); | |
} catch (Exception ex) { | |
results = new List<SObject>(); | |
} | |
} | |
// Obtain the RecordTypeInfos for this SObjectType token | |
Map<Id,Schema.RecordTypeInfo> recordTypeInfos = obj.getRecordTypeInfosByID(); | |
// Loop through all of the Record Types we found, | |
// and weed out those that are unavailable to the Running User | |
for (SObject rt : results) { | |
if (recordTypeInfos.get(rt.Id) != null) { | |
if (recordTypeInfos.get(rt.Id).isAvailable()) { | |
// This RecordType IS available to the running user, | |
// so add it to our map of RecordTypeIds by DeveloperName | |
mapRecordTypes.put(String.valueOf(rt.get('DeveloperName')),rt.Id); | |
} | |
else { | |
System.debug('The record type ' + rt.get('DeveloperName') + ' for object ' + rt.get('sObjectType') + ' is not availiable for the user.'); | |
} | |
} | |
} | |
return mapRecordTypes; | |
} | |
public static String currentTimeLong(){ | |
return System.now().format('MM/dd/yyyy hh:mm:ss a', UserInfo.getTimeZone().getID()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment