22 * Copyright (c), FinancialForce.com, inc
33 * All rights reserved.
44 *
5- * Redistribution and use in source and binary forms, with or without modification,
5+ * Redistribution and use in source and binary forms, with or without modification,
66 * are permitted provided that the following conditions are met:
77 *
8- * - Redistributions of source code must retain the above copyright notice,
8+ * - Redistributions of source code must retain the above copyright notice,
99 * this list of conditions and the following disclaimer.
10- * - Redistributions in binary form must reproduce the above copyright notice,
11- * this list of conditions and the following disclaimer in the documentation
10+ * - Redistributions in binary form must reproduce the above copyright notice,
11+ * this list of conditions and the following disclaimer in the documentation
1212 * and/or other materials provided with the distribution.
13- * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
14- * may be used to endorse or promote products derived from this software without
13+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
14+ * may be used to endorse or promote products derived from this software without
1515 * specific prior written permission.
1616 *
17- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
20- * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
17+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
20+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
2121 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
2222 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
2323 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
2828 * Provides an implementation of the Enterprise Application Architecture Unit Of Work, as defined by Martin Fowler
2929 * http://martinfowler.com/eaaCatalog/unitOfWork.html
3030 *
31- * "When you're pulling data in and out of a database, it's important to keep track of what you've changed; otherwise,
32- * that data won't be written back into the database. Similarly you have to insert new objects you create and
31+ * "When you're pulling data in and out of a database, it's important to keep track of what you've changed; otherwise,
32+ * that data won't be written back into the database. Similarly you have to insert new objects you create and
3333 * remove any objects you delete."
3434 *
35- * "You can change the database with each change to your object model, but this can lead to lots of very small database calls,
36- * which ends up being very slow. Furthermore it requires you to have a transaction open for the whole interaction, which is
35+ * "You can change the database with each change to your object model, but this can lead to lots of very small database calls,
36+ * which ends up being very slow. Furthermore it requires you to have a transaction open for the whole interaction, which is
3737 * impractical if you have a business transaction that spans multiple requests. The situation is even worse if you need to
3838 * keep track of the objects you've read so you can avoid inconsistent reads."
3939 *
40- * "A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done,
40+ * "A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done,
4141 * it figures out everything that needs to be done to alter the database as a result of your work."
4242 *
4343 * In an Apex context this pattern provides the following specific benifits
4444 * - Applies bulkfication to DML operations, insert, update and delete
4545 * - Manages a business transaction around the work and ensures a rollback occurs (even when exceptions are later handled by the caller)
46- * - Honours dependency rules between records and updates dependent relationships automatically during the commit
46+ * - Honours dependency rules between records and updates dependent relationships automatically during the commit
4747 *
48- * Please refer to the testMethod's in this class for example usage
48+ * Please refer to the testMethod's in this class for example usage
4949 *
5050 * TODO: Need to complete the 100% coverage by covering parameter exceptions in tests
5151 * TODO: Need to add some more test methods for more complex use cases and some unexpected (e.g. registerDirty and then registerDeleted)
@@ -55,25 +55,25 @@ public virtual class fflib_SObjectUnitOfWork
5555 implements fflib_ISObjectUnitOfWork
5656{
5757 private List <Schema .SObjectType > m_sObjectTypes = new List <Schema .SObjectType >();
58-
58+
5959 private Map <String , List <SObject >> m_newListByType = new Map <String , List <SObject >>();
60-
60+
6161 private Map <String , Map <Id , SObject >> m_dirtyMapByType = new Map <String , Map <Id , SObject >>();
62-
62+
6363 private Map <String , Map <Id , SObject >> m_deletedMapByType = new Map <String , Map <Id , SObject >>();
64-
64+
6565 private Map <String , Relationships > m_relationships = new Map <String , Relationships >();
6666
6767 private List <IDoWork > m_workList = new List <IDoWork >();
6868
6969 private SendEmailWork m_emailWork = new SendEmailWork ();
70-
70+
7171 private IDML m_dml ;
72-
72+
7373 /**
7474 * Interface describes work to be performed during the commitWork method
7575 **/
76- public interface IDoWork
76+ public interface IDoWork
7777 {
7878 void doWork ();
7979 }
@@ -84,7 +84,7 @@ public virtual class fflib_SObjectUnitOfWork
8484 void dmlUpdate (List <SObject > objList );
8585 void dmlDelete (List <SObject > objList );
8686 }
87-
87+
8888 public class SimpleDML implements IDML
8989 {
9090 public void dmlInsert (List <SObject > objList ){
@@ -111,17 +111,17 @@ public virtual class fflib_SObjectUnitOfWork
111111 public fflib_SObjectUnitOfWork (List <Schema .SObjectType > sObjectTypes , IDML dml )
112112 {
113113 m_sObjectTypes = sObjectTypes .clone ();
114-
114+
115115 for (Schema .SObjectType sObjectType : m_sObjectTypes )
116116 {
117117 m_newListByType .put (sObjectType .getDescribe ().getName (), new List <SObject >());
118118 m_dirtyMapByType .put (sObjectType .getDescribe ().getName (), new Map <Id , SObject >());
119119 m_deletedMapByType .put (sObjectType .getDescribe ().getName (), new Map <Id , SObject >());
120- m_relationships .put (sObjectType .getDescribe ().getName (), new Relationships ());
120+ m_relationships .put (sObjectType .getDescribe ().getName (), new Relationships ());
121121 }
122122
123123 m_workList .add (m_emailWork );
124-
124+
125125 m_dml = dml ;
126126 }
127127
@@ -140,7 +140,7 @@ public virtual class fflib_SObjectUnitOfWork
140140 {
141141 m_emailWork .registerEmail (email );
142142 }
143-
143+
144144 /**
145145 * Register a newly created SObject instance to be inserted when commitWork is called
146146 *
@@ -165,7 +165,7 @@ public virtual class fflib_SObjectUnitOfWork
165165 }
166166
167167 /**
168- * Register a newly created SObject instance to be inserted when commitWork is called,
168+ * Register a newly created SObject instance to be inserted when commitWork is called,
169169 * you may also provide a reference to the parent record instance (should also be registered as new separatly)
170170 *
171171 * @param record A newly created SObject instance to be inserted during commitWork
@@ -176,16 +176,16 @@ public virtual class fflib_SObjectUnitOfWork
176176 {
177177 if (record .Id != null )
178178 throw new UnitOfWorkException (' Only new records can be registered as new' );
179- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
179+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
180180 if (! m_newListByType .containsKey (sObjectType ))
181181 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
182- m_newListByType .get (sObjectType ).add (record );
182+ m_newListByType .get (sObjectType ).add (record );
183183 if (relatedToParentRecord != null && relatedToParentField != null )
184184 registerRelationship (record , relatedToParentField , relatedToParentRecord );
185185 }
186-
186+
187187 /**
188- * Register a relationship between two records that have yet to be inserted to the database. This information will be
188+ * Register a relationship between two records that have yet to be inserted to the database. This information will be
189189 * used during the commitWork phase to make the references only when related records have been inserted to the database.
190190 *
191191 * @param record An existing or newly created record
@@ -194,12 +194,12 @@ public virtual class fflib_SObjectUnitOfWork
194194 */
195195 public void registerRelationship (SObject record , Schema.sObjectField relatedToField , SObject relatedTo )
196196 {
197- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
197+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
198198 if (! m_newListByType .containsKey (sObjectType ))
199199 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
200200 m_relationships .get (sObjectType ).add (record , relatedToField , relatedTo );
201201 }
202-
202+
203203 /**
204204 * Register an existing record to be updated during the commitWork method
205205 *
@@ -209,12 +209,32 @@ public virtual class fflib_SObjectUnitOfWork
209209 {
210210 if (record .Id == null )
211211 throw new UnitOfWorkException (' New records cannot be registered as dirty' );
212- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
212+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
213213 if (! m_dirtyMapByType .containsKey (sObjectType ))
214214 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
215- m_dirtyMapByType .get (sObjectType ).put (record .Id , record );
215+ m_dirtyMapByType .get (sObjectType ).put (record .Id , record );
216216 }
217217
218+ /**
219+ * Register an existing record to be updated when commitWork is called,
220+ * you may also provide a reference to the parent record instance (should also be registered as new separatly)
221+ *
222+ * @param record A newly created SObject instance to be inserted during commitWork
223+ * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent
224+ * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separatly)
225+ **/
226+ public void registerDirty (SObject record , Schema.sObjectField relatedToParentField , SObject relatedToParentRecord )
227+ {
228+ if (record .Id == null )
229+ throw new UnitOfWorkException (' New records cannot be registered as dirty' );
230+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
231+ if (! m_dirtyMapByType .containsKey (sObjectType ))
232+ throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
233+ m_dirtyMapByType .get (sObjectType ).put (record .Id , record );
234+ if (relatedToParentRecord != null && relatedToParentField != null )
235+ registerRelationship (record , relatedToParentField , relatedToParentRecord );
236+ }
237+
218238 /**
219239 * Register a list of existing records to be updated during the commitWork method
220240 *
@@ -237,12 +257,12 @@ public virtual class fflib_SObjectUnitOfWork
237257 {
238258 if (record .Id == null )
239259 throw new UnitOfWorkException (' New records cannot be registered for deletion' );
240- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
260+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
241261 if (! m_deletedMapByType .containsKey (sObjectType ))
242262 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
243- m_deletedMapByType .get (sObjectType ).put (record .Id , record );
263+ m_deletedMapByType .get (sObjectType ).put (record .Id , record );
244264 }
245-
265+
246266 /**
247267 * Register a list of existing records to be deleted during the commitWork method
248268 *
@@ -255,25 +275,25 @@ public virtual class fflib_SObjectUnitOfWork
255275 this .registerDeleted (record );
256276 }
257277 }
258-
278+
259279 /**
260280 * Takes all the work that has been registered with the UnitOfWork and commits it to the database
261281 **/
262282 public void commitWork ()
263283 {
264- // Wrap the work in its own transaction
265- Savepoint sp = Database .setSavePoint ();
284+ // Wrap the work in its own transaction
285+ Savepoint sp = Database .setSavePoint ();
266286 try
267- {
287+ {
268288 // Insert by type
269289 for (Schema .SObjectType sObjectType : m_sObjectTypes )
270290 {
271291 m_relationships .get (sObjectType .getDescribe ().getName ()).resolve ();
272292 m_dml .dmlInsert (m_newListByType .get (sObjectType .getDescribe ().getName ()));
273- }
293+ }
274294 // Update by type
275295 for (Schema .SObjectType sObjectType : m_sObjectTypes )
276- m_dml .dmlUpdate (m_dirtyMapByType .get (sObjectType .getDescribe ().getName ()).values ());
296+ m_dml .dmlUpdate (m_dirtyMapByType .get (sObjectType .getDescribe ().getName ()).values ());
277297 // Delete by type (in reverse dependency order)
278298 Integer objectIdx = m_sObjectTypes .size () - 1 ;
279299 while (objectIdx >= 0 )
@@ -290,7 +310,7 @@ public virtual class fflib_SObjectUnitOfWork
290310 throw e ;
291311 }
292312 }
293-
313+
294314 private class Relationships
295315 {
296316 private List <Relationship > m_relationships = new List <Relationship >();
@@ -301,7 +321,7 @@ public virtual class fflib_SObjectUnitOfWork
301321 for (Relationship relationship : m_relationships )
302322 relationship .Record .put (relationship .RelatedToField , relationship .RelatedTo .Id );
303323 }
304-
324+
305325 public void add (SObject record , Schema.sObjectField relatedToField , SObject relatedTo )
306326 {
307327 // Relationship to resolve
@@ -312,20 +332,20 @@ public virtual class fflib_SObjectUnitOfWork
312332 m_relationships .add (relationship );
313333 }
314334 }
315-
335+
316336 private class Relationship
317337 {
318338 public SObject Record ;
319339 public Schema.sObjectField RelatedToField ;
320340 public SObject RelatedTo ;
321341 }
322-
342+
323343 /**
324344 * UnitOfWork Exception
325345 **/
326346 public class UnitOfWorkException extends Exception {}
327347
328- /**
348+ /**
329349 * Internal implementation of Messaging.sendEmail, see outer class registerEmail method
330350 **/
331351 private class SendEmailWork implements IDoWork
@@ -346,5 +366,5 @@ public virtual class fflib_SObjectUnitOfWork
346366 {
347367 if (emails .size () > 0 ) Messaging .sendEmail (emails );
348368 }
349- }
369+ }
350370}
0 commit comments