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,15 +111,15 @@ 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 // register the type
118118 handleRegisterType (sObjectType );
119119 }
120120
121121 m_workList .add (m_emailWork );
122-
122+
123123 m_dml = dml ;
124124 }
125125
@@ -166,7 +166,7 @@ public virtual class fflib_SObjectUnitOfWork
166166 {
167167 m_emailWork .registerEmail (email );
168168 }
169-
169+
170170 /**
171171 * Register a newly created SObject instance to be inserted when commitWork is called
172172 *
@@ -191,7 +191,7 @@ public virtual class fflib_SObjectUnitOfWork
191191 }
192192
193193 /**
194- * Register a newly created SObject instance to be inserted when commitWork is called,
194+ * Register a newly created SObject instance to be inserted when commitWork is called,
195195 * you may also provide a reference to the parent record instance (should also be registered as new separatly)
196196 *
197197 * @param record A newly created SObject instance to be inserted during commitWork
@@ -202,16 +202,16 @@ public virtual class fflib_SObjectUnitOfWork
202202 {
203203 if (record .Id != null )
204204 throw new UnitOfWorkException (' Only new records can be registered as new' );
205- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
205+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
206206 if (! m_newListByType .containsKey (sObjectType ))
207207 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
208- m_newListByType .get (sObjectType ).add (record );
208+ m_newListByType .get (sObjectType ).add (record );
209209 if (relatedToParentRecord != null && relatedToParentField != null )
210210 registerRelationship (record , relatedToParentField , relatedToParentRecord );
211211 }
212-
212+
213213 /**
214- * Register a relationship between two records that have yet to be inserted to the database. This information will be
214+ * Register a relationship between two records that have yet to be inserted to the database. This information will be
215215 * used during the commitWork phase to make the references only when related records have been inserted to the database.
216216 *
217217 * @param record An existing or newly created record
@@ -220,12 +220,12 @@ public virtual class fflib_SObjectUnitOfWork
220220 */
221221 public void registerRelationship (SObject record , Schema.sObjectField relatedToField , SObject relatedTo )
222222 {
223- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
223+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
224224 if (! m_newListByType .containsKey (sObjectType ))
225225 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
226226 m_relationships .get (sObjectType ).add (record , relatedToField , relatedTo );
227227 }
228-
228+
229229 /**
230230 * Register an existing record to be updated during the commitWork method
231231 *
@@ -235,12 +235,32 @@ public virtual class fflib_SObjectUnitOfWork
235235 {
236236 if (record .Id == null )
237237 throw new UnitOfWorkException (' New records cannot be registered as dirty' );
238- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
238+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
239239 if (! m_dirtyMapByType .containsKey (sObjectType ))
240240 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
241- m_dirtyMapByType .get (sObjectType ).put (record .Id , record );
241+ m_dirtyMapByType .get (sObjectType ).put (record .Id , record );
242242 }
243243
244+ /**
245+ * Register an existing record to be updated when commitWork is called,
246+ * you may also provide a reference to the parent record instance (should also be registered as new separatly)
247+ *
248+ * @param record A newly created SObject instance to be inserted during commitWork
249+ * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent
250+ * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separatly)
251+ **/
252+ public void registerDirty (SObject record , Schema.sObjectField relatedToParentField , SObject relatedToParentRecord )
253+ {
254+ if (record .Id == null )
255+ throw new UnitOfWorkException (' New records cannot be registered as dirty' );
256+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
257+ if (! m_dirtyMapByType .containsKey (sObjectType ))
258+ throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
259+ m_dirtyMapByType .get (sObjectType ).put (record .Id , record );
260+ if (relatedToParentRecord != null && relatedToParentField != null )
261+ registerRelationship (record , relatedToParentField , relatedToParentRecord );
262+ }
263+
244264 /**
245265 * Register a list of existing records to be updated during the commitWork method
246266 *
@@ -263,12 +283,12 @@ public virtual class fflib_SObjectUnitOfWork
263283 {
264284 if (record .Id == null )
265285 throw new UnitOfWorkException (' New records cannot be registered for deletion' );
266- String sObjectType = record .getSObjectType ().getDescribe ().getName ();
286+ String sObjectType = record .getSObjectType ().getDescribe ().getName ();
267287 if (! m_deletedMapByType .containsKey (sObjectType ))
268288 throw new UnitOfWorkException (String .format (' SObject type {0} is not supported by this unit of work' , new String [] { sObjectType }));
269- m_deletedMapByType .get (sObjectType ).put (record .Id , record );
289+ m_deletedMapByType .get (sObjectType ).put (record .Id , record );
270290 }
271-
291+
272292 /**
273293 * Register a list of existing records to be deleted during the commitWork method
274294 *
@@ -281,7 +301,7 @@ public virtual class fflib_SObjectUnitOfWork
281301 this .registerDeleted (record );
282302 }
283303 }
284-
304+
285305 /**
286306 * Takes all the work that has been registered with the UnitOfWork and commits it to the database
287307 **/
@@ -290,22 +310,22 @@ public virtual class fflib_SObjectUnitOfWork
290310 // notify we're starting the commit work
291311 onCommitWorkStarting ();
292312
293- // Wrap the work in its own transaction
313+ // Wrap the work in its own transaction
294314 Savepoint sp = Database .setSavePoint ();
295315 Boolean wasSuccessful = false ;
296316 try
297- {
317+ {
298318 // notify we're starting the DML operations
299319 onDMLStarting ();
300320 // Insert by type
301321 for (Schema .SObjectType sObjectType : m_sObjectTypes )
302322 {
303323 m_relationships .get (sObjectType .getDescribe ().getName ()).resolve ();
304324 m_dml .dmlInsert (m_newListByType .get (sObjectType .getDescribe ().getName ()));
305- }
325+ }
306326 // Update by type
307327 for (Schema .SObjectType sObjectType : m_sObjectTypes )
308- m_dml .dmlUpdate (m_dirtyMapByType .get (sObjectType .getDescribe ().getName ()).values ());
328+ m_dml .dmlUpdate (m_dirtyMapByType .get (sObjectType .getDescribe ().getName ()).values ());
309329 // Delete by type (in reverse dependency order)
310330 Integer objectIdx = m_sObjectTypes .size () - 1 ;
311331 while (objectIdx >= 0 )
@@ -340,7 +360,7 @@ public virtual class fflib_SObjectUnitOfWork
340360 onCommitWorkFinished (wasSuccessful );
341361 }
342362 }
343-
363+
344364 private class Relationships
345365 {
346366 private List <Relationship > m_relationships = new List <Relationship >();
@@ -351,7 +371,7 @@ public virtual class fflib_SObjectUnitOfWork
351371 for (Relationship relationship : m_relationships )
352372 relationship .Record .put (relationship .RelatedToField , relationship .RelatedTo .Id );
353373 }
354-
374+
355375 public void add (SObject record , Schema.sObjectField relatedToField , SObject relatedTo )
356376 {
357377 // Relationship to resolve
@@ -362,20 +382,20 @@ public virtual class fflib_SObjectUnitOfWork
362382 m_relationships .add (relationship );
363383 }
364384 }
365-
385+
366386 private class Relationship
367387 {
368388 public SObject Record ;
369389 public Schema.sObjectField RelatedToField ;
370390 public SObject RelatedTo ;
371391 }
372-
392+
373393 /**
374394 * UnitOfWork Exception
375395 **/
376396 public class UnitOfWorkException extends Exception {}
377397
378- /**
398+ /**
379399 * Internal implementation of Messaging.sendEmail, see outer class registerEmail method
380400 **/
381401 private class SendEmailWork implements IDoWork
@@ -396,5 +416,5 @@ public virtual class fflib_SObjectUnitOfWork
396416 {
397417 if (emails .size () > 0 ) Messaging .sendEmail (emails );
398418 }
399- }
419+ }
400420}
0 commit comments