diff --git a/fflib/src/classes/fflib_SObjectUnitOfWork.cls b/fflib/src/classes/fflib_SObjectUnitOfWork.cls index aa5a450472b..28f98725860 100644 --- a/fflib/src/classes/fflib_SObjectUnitOfWork.cls +++ b/fflib/src/classes/fflib_SObjectUnitOfWork.cls @@ -131,14 +131,14 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerNew(SObject record, Schema.sObjectField relatedToParentField, SObject relatedToParentRecord) { - if(record.Id != null) - throw new UnitOfWorkException('Only new records can be registered as new'); + // Need to think about how to handle this + //if(record.Id != null) + // throw new UnitOfWorkException('Only new records can be registered as new'); String sObjectType = record.getSObjectType().getDescribe().getName(); if(!m_newListByType.containsKey(sObjectType)) throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); m_newListByType.get(sObjectType).add(record); - if(relatedToParentRecord!=null && relatedToParentField!=null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); + registerRelationship(record, relatedToParentField, relatedToParentRecord); } /** @@ -154,7 +154,10 @@ public virtual class fflib_SObjectUnitOfWork String sObjectType = record.getSObjectType().getDescribe().getName(); if(!m_newListByType.containsKey(sObjectType)) throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); - m_relationships.get(sObjectType).add(record, relatedToField, relatedTo); + if(relatedTo!=null && relatedToField!=null) + m_relationships.get(sObjectType).add(record, relatedToField, relatedTo); + else + m_relationships.get(sObjectType).add(record); } /** @@ -199,8 +202,12 @@ public virtual class fflib_SObjectUnitOfWork // Insert by type for(Schema.SObjectType sObjectType : m_sObjectTypes) { - m_relationships.get(sObjectType.getDescribe().getName()).resolve(); - insert m_newListByType.get(sObjectType.getDescribe().getName()); + List resolved; + + while((resolved = m_relationships.get(sObjectType.getDescribe().getName()).resolve()).size() > 0) + { + insert resolved; + } } // Update by type for(Schema.SObjectType sObjectType : m_sObjectTypes) @@ -224,15 +231,79 @@ public virtual class fflib_SObjectUnitOfWork private class Relationships { - private List m_relationships = new List(); + private SObjectRelationshipMap m_SObjectRelationshipMap = new SObjectRelationshipMap(/*128, 0.8*/); - public void resolve() + public List resolve() { // Resolve relationships - for(Relationship relationship : m_relationships) - relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); + List fullyResolved = new List(); + + for(SObjectRelationshipMapIterator i = m_SObjectRelationshipMap.iterator(); i.hasNext();) + { + SObjectRelationshipMapEntry entry = i.next(); + SObjectReferenceWrapper obj = entry.getKey(); + List objRelationships = entry.getValue(); + + // Cache the Id to allow editing of fields that normally get locked + // for editing after insert + String cachedId = String.valueOf(obj.Record.Id); + obj.Record.Id = null; + + // Resolve as many relationships as possible + for(Relationship relationship : objRelationships) + { + if(relationship.RelatedTo.Id != null && !fakeIds.contains(relationship.RelatedTo.Id)) + { + relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); + relationship.Resolved = true; + } + } + + // Retore the Id + obj.Record.Id = cachedId; + + Integer j = 0; + + // Clear out any resolved relationships + while (j < objRelationships.size()) + { + if(objRelationships.get(j).Resolved) + { + objRelationships.remove(j); + } + else + { + j++; + } + } + + // If all relationships are resolved add it to the list to be returned + // and remove it from future processing + if(objRelationships.size() == 0) + { + m_SObjectRelationshipMap.remove(obj); + + obj.Record.Id = null; + fullyResolved.add(obj.Record); + } + } + + return fullyResolved; } + public void add(SObject record) + { + // Object with no relationship + SObjectReferenceWrapper obj = new SObjectReferenceWrapper(record); + List rels = m_SObjectRelationshipMap.get(obj); + + if(rels == null) + { + rels = new List(); + m_SObjectRelationshipMap.put(obj, rels); + } + } + public void add(SObject record, Schema.sObjectField relatedToField, SObject relatedTo) { // Relationship to resolve @@ -240,7 +311,18 @@ public virtual class fflib_SObjectUnitOfWork relationship.Record = record; relationship.RelatedToField = relatedToField; relationship.RelatedTo = relatedTo; - m_relationships.add(relationship); + relationship.Resolved = false; + + SObjectReferenceWrapper obj = new SObjectReferenceWrapper(record); + List rels = m_SObjectRelationshipMap.get(obj); + + if(rels == null) + { + rels = new List(); + m_SObjectRelationshipMap.put(obj, rels); + } + + rels.add(relationship); } } @@ -249,6 +331,344 @@ public virtual class fflib_SObjectUnitOfWork public SObject Record; public Schema.sObjectField RelatedToField; public SObject RelatedTo; + public Boolean Resolved; + } + + static Integer s_num = 1; + // Keep a list of the fakeIds used, this is by no means foolproof as they could clash with a real + // Id, there has to be a better way + static Set fakeIds = new Set(); + + public static String getFakeId(Schema.SObjectType sot) + { + String result = String.valueOf(s_num++); + String id = sot.getDescribe().getKeyPrefix() + '0'.repeat(12-result.length()) + result; + fakeIds.add(id); + return id; + } + + private class SObjectReferenceWrapper + { + public SObject Record; + private Integer hashCode; + + public SObjectReferenceWrapper(SObject record) + { + this.Record = record; + + // Id is the only field guaranteed to be on every object + // so keep a unique reference in it + // We can't use the standard SObject hashCode to determine + // unique records as it's based purely on field values, if there + // was a way of getting the memory address this could work around it + if(record.Id == null) + { + record.Id = getFakeId(record.getSObjectType()); + } + + // We need a hashCode for fast map put operations, cache the result + // Use the String hashCode of the Id as System.hashCode(object.Id) does not + // return the same result on every call for an object + hashCode = String.valueOf(record.Id).hashCode(); + } + + // Fallback for hash collisions, compared the memory address of objects using exact equality operator + public Boolean equals(Object obj) + { + if (obj instanceof SObjectReferenceWrapper) + { + SObjectReferenceWrapper o = (SObjectReferenceWrapper)obj; + return (Record === o.Record); + } + + return false; + } + + public Integer hashCode() + { + return hashCode; + } + } + + /* Internal hashmap implementation specificly for this class, this is to work around a bug where + * Salesforce does not always use hashCode when it's implemented on classes (even though it should) */ + private class SObjectRelationshipMap + { + private final Integer DEFAULT_INITIAL_CAPACITY = 16; + private final Integer MAXIMUM_CAPACITY = 1 << 30; + private final Decimal DEFAULT_LOAD_FACTOR = 0.75; + + private SObjectRelationshipMapEntry[] table; + private Integer size; + private Integer threshold; + private final Decimal loadFactor; + + public SObjectRelationshipMap() + { + this.size = 0; + this.loadFactor = DEFAULT_LOAD_FACTOR; + this.threshold = (Integer)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); + this.table = new SObjectRelationshipMapEntry[DEFAULT_INITIAL_CAPACITY]; + } + + // internal utilities + private Integer hash(Integer h) + { + // This function ensures that hashCodes that differ only by + // constant multiples at each bit position have a bounded + // number of collisions (approximately 8 at default load factor). + h ^= (h >>> 20) ^ (h >>> 12); + return (h ^ (h >>> 7)) ^ (h >>> 4); + } + + private Integer indexFor(Integer h, Integer length) + { + return h & (length-1); + } + + // public interface + public List get(SObjectReferenceWrapper key) + { + Integer hash = hash(key.hashCode()); + Integer length = table.size(); + + for (SObjectRelationshipMapEntry e = table[indexFor(hash, length)]; e != null; e = e.next) + { + SObjectReferenceWrapper k; + + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) + { + return e.value; + } + } + + return null; + } + + public List put(SObjectReferenceWrapper key, List value) + { + Integer hash = hash(key.hashCode()); + Integer i = indexFor(hash, table.size()); + + for (SObjectRelationshipMapEntry e = table[i]; e != null; e = e.next) + { + SObjectReferenceWrapper k; + + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) + { + List oldValue = e.value; + e.value = value; + return oldValue; + } + } + + addEntry(hash, key, value, i); + return null; + } + + private void resize(Integer newCapacity) + { + SObjectRelationshipMapEntry[] oldTable = table; + Integer oldCapacity = oldTable.size(); + + if (oldCapacity == MAXIMUM_CAPACITY) + { + threshold = 2147483647; // Used since Integer.MAX_VALUE is not available in Apex + return; + } + + SObjectRelationshipMapEntry[] newTable = new SObjectRelationshipMapEntry[newCapacity]; + transfer(newTable); + table = newTable; + threshold = (Integer)(newCapacity * loadFactor); + } + + private void transfer(SObjectRelationshipMapEntry[] newTable) + { + SObjectRelationshipMapEntry[] src = table; + Integer newCapacity = newTable.size(); + Integer length = src.size(); + + for (Integer j = 0; j < length; j++) + { + SObjectRelationshipMapEntry e = src[j]; + + if (e != null) + { + src[j] = null; + + do + { + SObjectRelationshipMapEntry next = e.next; + Integer i = indexFor(e.hash, newCapacity); + e.next = newTable[i]; + newTable[i] = e; + e = next; + } + while (e != null); + } + } + } + + public List remove(SObjectReferenceWrapper key) + { + SObjectRelationshipMapEntry e = removeEntryForKey(key); + return (e == null ? null : e.value); + } + + private void addEntry(Integer hash, SObjectReferenceWrapper key, List value, Integer bucketIndex) + { + SObjectRelationshipMapEntry e = table[bucketIndex]; + table[bucketIndex] = new SObjectRelationshipMapEntry(hash, key, value, e); + + if (size++ >= threshold) + { + resize(2 * table.size()); + } + } + + private SObjectRelationshipMapEntry removeEntryForKey(SObjectReferenceWrapper key) + { + Integer hash = (key == null) ? 0 : hash(key.hashCode()); + Integer i = indexFor(hash, table.size()); + SObjectRelationshipMapEntry prev = table[i]; + SObjectRelationshipMapEntry e = prev; + + while (e != null) + { + SObjectRelationshipMapEntry next = e.next; + SObjectReferenceWrapper k; + + if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) + { + size--; + + if (prev == e) + { + table[i] = next; + } + else + { + prev.next = next; + } + + return e; + } + + prev = e; + e = next; + } + + return e; + } + + + + public SObjectRelationshipMapIterator iterator() + { + return new SObjectRelationshipMapIterator(this); + } + } + + private class SObjectRelationshipMapEntry + { + private final SObjectReferenceWrapper key; + private List value; + private SObjectRelationshipMapEntry next; + private final Integer hash; + + private SObjectRelationshipMapEntry(Integer h, SObjectReferenceWrapper k, List v, SObjectRelationshipMapEntry n) + { + this.value = v; + this.next = n; + this.key = k; + this.hash = h; + } + + public SObjectReferenceWrapper getKey() + { + return key; + } + + public List getValue() + { + return value; + } + + public Boolean equals(Object o) + { + if (!(o instanceof SObjectRelationshipMapEntry)) + return false; + + SObjectRelationshipMapEntry e = (SObjectRelationshipMapEntry)o; + SObjectReferenceWrapper k1 = getKey(); + SObjectReferenceWrapper k2 = e.getKey(); + + if (k1 == k2 || (k1 != null && k1.equals(k2))) + { + List v1 = getValue(); + List v2 = e.getValue(); + + if (v1 == v2 || (v1 != null && v1.equals(v2))) + { + return true; + } + } + + return false; + } + } + + private class SObjectRelationshipMapIterator implements System.Iterator + { + private SObjectRelationshipMap m; + + private SObjectRelationshipMapEntry next; // next entry to return + private Integer index; // current slot + private SObjectRelationshipMapEntry current; // current entry + + private SObjectRelationshipMapIterator(SObjectRelationshipMap m) + { + this.m = m; + this.index = 0; + + if (m.size > 0) + { // advance to first entry + SObjectRelationshipMapEntry[] t = m.table; + Integer length = t.size(); + + while (index < length && (next = t[index++]) == null) + ; + } + } + + public Boolean hasNext() + { + return next != null; + } + + public SObjectRelationshipMapEntry next() + { + SObjectRelationshipMapEntry e = next; + + if (e == null) + { + throw new UnitOfWorkException/*NoSuchElementException*/(); + } + + if ((next = e.next) == null) + { + SObjectRelationshipMapEntry[] t = m.table; + Integer length = t.size(); + + while (index < length && (next = t[index++]) == null) + ; + } + + current = e; + + return e; + } } /** @@ -278,4 +698,4 @@ public virtual class fflib_SObjectUnitOfWork Messaging.sendEmail(emails); } } -} \ No newline at end of file +}