The UnitOfWork2 class
Sometimes actions on entities span a longer timeframe and / or multiple screens. It's then often impossible to start a database transaction as user-interaction during a transaction should be avoided. To track all the changes made and to persist them in one transaction can then be a tedious task.
With the UnitOfWork2 class this can be solved. The UnitOfWork2 class lets you collect actions on entities or collections of entities and can perform these actions in one atomic action. The UnitOfWork2 class is binary serializable which means it can travel across remoting boundaries.
Entities and entity collections added to the UnitOfWork2 class are not aware that they're added to that class, so if you decide not to continue with a given UnitOfWork2 instance you can simply let it get out of scope. UnitOfWork2 objects figure out the order in which actions have to be performed automatically: first Inserts, then Updates and then Deletes. This is configurable, see the section below about Specifying the order in which the actions are executed.
Single entities
A UnitOfWork2 class can work with single entities or collections of entities. This paragraph discusses the UnitOfWork2 class with single entities. Actions collected by the UnitOfWork2 class are not yet performed, but are performed when the UnitOfWork2's Commit() method is called.
The UnitOfWork2 class works with Add methods for an entity and a given action: AddForSave() or AddForDelete(). You can specify additional parameters for the action: recursive saves and save / delete restriction filters. A delete action for a new entity is ignored.
The following example illustrates both methods. First a recursive save is added and after that a delete action. The actions are not executed until Commit() is called. Commit() always expects a DataAccessAdapter object which is used to run the persistent actions. You don't have to start a transaction yourself, if the passed in DataAccessAdapter is not controlling a transaction yet, a new one is started.
Commit() can also auto-commit the transaction, if all the actions succeed, you then have to use the overload of Commit() which expects a boolean, autoCommit. You can commit more than one UnitOfWork2 object in one transaction, simply pass the same DataAccessAdapter object to all Commit() calls, passing false for autoCommit.
var newCustomer = new CustomerEntity();
// ... fill newCustomer's data
var newAddress = new AddressEntity();
// ... fill newAddress's data
newCustomer.VisitingAddress = newAddress;
newCustomer.BillingAddress = newAddress;
var uow = new UnitOfWork2();
// add the customer for a recursive save action and specify true
// so the entity is refetched after the save action.
uow.AddForSave(newCustomer, true);
var productToDelete = new ProductEntity(productID);
using(var adapter = new DataAccessAdapter())
{
adapter.FetchEntity(productToDelete);
// add this product for deletion.
uow.AddForDelete(productToDelete);
// commit all actions in one go
uow.Commit(adapter, true);
}
Dim newCustomer As New CustomerEntity()
' ... fill newCustomer's data
Dim newAddress As New AddressEntity()
' ... fill newAddress's data
newCustomer.VisitingAddress = newAddress
newCustomer.BillingAddress = newAddress
Dim uow As New UnitOfWork2()
' add the customer for a recursive save action and specify true
' so the entity is refetched after the save action.
uow.AddForSave(newCustomer, True)
Dim productToDelete As New ProductEntity(productID)
Using adapter As New DataAccessAdapter()
adapter.FetchEntity(productToDelete)
' add this product for deletion.
uow.AddForDelete(productToDelete)
' commit all actions in one go
uow.Commit(adapter, True)
End Using
After the Commit() action, the database has two new entities, the customer and the address, and the product entity is deleted. These actions are taken place inside a new transaction and when Commit() was called, which auto-commits the transaction at the end of the actions.
Entity collections
Sometimes a complete collection of entities has to be saved, or deleted. Instead of adding all entities individually, you can add a collection in one go for a given action: save or delete. The following example loads an Order and its OrderDetail entities and deletes the OrderDetail entities while updating the Order entity.
The entities in the collection are examined when Commit() is called. This means that an entity which is in the collection when the collection is added to the UnitOfWork2 object and is removed from that collection after that action but before Commit() is called, is not processed by the UnitOfWork2, as the entity is no longer part of the collection being processed.
var order = new OrderEntity(10254);
// load order detail entities through a prefetch Path
var prefetchPath = new PrefetchPath2(EntityType.OrderEntity);
prefetchPath.Add(OrderEntity.PrefetchPathOrderDetails);
using(var adapter = new DataAccessAdapter())
{
adapter.FetchEntity(order, prefetchPath);
// alter order
order.EmployeeID = 3;
var uow = new UnitOfWork2();
// add the order for save, no recursion.
uow.AddForSave(order);
uow.AddCollectionForDelete(order.OrderDetails);
// commit all actions in one go
uow.Commit(adapter, true);
}
Dim order As New OrderEntity(10254)
' load order detail entities through a prefetch Path
Dim prefetchPath As New PrefetchPath2(EntityType.OrderEntity)
prefetchPath.Add(OrderEntity.PrefetchPathOrderDetails)
Using adapter As New DataAccessAdapter()
adapter.FetchEntity(order, prefetchPath)
' alter order
order.EmployeeID = 3
Dim uow As New UnitOfWork2()
' add the order for save, no recursion.
uow.AddForSave(order)
uow.AddCollectionForDelete(order.OrderDetails)
' commit all actions in one go
uow.Commit(adapter, True)
End Using
When Commit() is called, first all entities in the collection added are added as entities for the Delete action. After that, all actions are executed, first the save action, then the deletes.
Stored procedures
The UnitOfWork2 class is able to collect calls to stored procedures as well, and lets you schedule these calls with the work already added to the UnitOfWork2 class, using four slots. The support for stored procedure calls is done through delegates.
This means that you can use this feature also for your own methods, as long as there is a delegate defined for that method. If you want to receive the actual DataAccessAdapter object passed into the method, you have to make sure the method accepts a DataAccessAdapter object as the last parameter.
Adding a stored procedure call can only be done for Action procedure calls. To add a stored procedure call, you'll use the AddCallBack() method, which accepts a System.Delegate object, a slot enum value which schedules the call, and zero or more parameters. Below are the slot definitions listed on which you can schedule a stored procedure call.
Enum Value | Description |
---|---|
PreEntityInsert | Execute the callback before the first entity is inserted. |
PreEntityUpdate | Execute the callback after the last entity has been inserted but before the first entity will be updated. |
PreEntityDelete | Execute the callback after the last entity has been updated but before the first entity will be deleted. |
PostEntityDelete | Execute the callback after the last entity has been deleted. |
LLBLGen Pro generates for each Action procedure call a Delegate definition. Using such a generated delegate definition, you could add a call to a stored procedure using the following code. It adds a call to the ClearTestRunData stored procedure.
It specifies that the DataAccessAdapter has to be passed into the procedure so the call will run in the same transaction as the rest of the calls the UnitOfWork2 object will make. If that's not done, the action procedure will create it's own DataAccessAdapter object. The call is scheduled right before the Delete calls are made on entities.
var uow = new UnitOfWork2();
uow.AddCallBack(new ActionProcedures.ClearTestRunDataCallBack(ActionProcedures.ClearTestRunData),
UnitOfWorkCallBackScheduleSlot.PreEntityDelete, true, _testRunID);
DeleteEntitiesDirectly and UpdateEntitiesDirectly
Besides adding calls to stored procedures, the UnitOfWork2 object can also accept calls to DeleteEntitiesDirectly() and UpdateEntitiesDirectly(). You add calls to one of these methods by using one of the overloads of AddDeleteEntitiesDirectlyCall or AddUpdateEntitiesDirectlyCall, by specifying the required parameters for these calls as if you'd make them directly.
The calls will be executed on the DataAccessAdapter object used by Commit. The DeleteEntitiesDirectly call will be executed after the entity delete actions but before the PostEntityDelete callbacks. The UpdateEntitiesDirectly call will be executed after the last entity has been updated but before the PreEntityUpdate callbacks.
Specifying the order in which the actions are executed
The UnitOfWork2 class typically executes the actions added to it in the following order: CallBacks for the PreEntityInsert slot, Inserts, CallBacks for the PreEntityUpdate slot, Updates, UpdateEntitiesDirectly calls, CallBacks for the PreEntityDelete slot, Deletes, CallBacks for the PostEntityDelete slot, DeleteEntitiesDirectly calls.
This order can be too limited, for example if you first have to delete an entity before a new insert can take place because the entity to insert has the same value for a field with a unique constraint.
LLBLGen Pro lets you define entity oriented actions to be ordered in a different order, so the UnitOfWork2 class will for example first execute the deletes and then the updates. This is done by specifying a list of UnitOfWorkBlockType values for the property UnitOfWork2.CommitOrder.
By default you don't have to specify any commit order, the UnitOfWork2 class will follow the sequence as specified above. However as soon as you specify a list of UnitOfWorkBlockType values for CommitOrder, it will use that list instead. This means that if you omit a block type, these actions aren't executed at all. Duplicates are filtered out so specifying a blocktype twice has no effect, the second one is ignored.
CallBacks with the name Preaction or Postaction belong to the blocktype of action and will be executed in that block, in the same order as described above, so for example PreUpdateEntity callbacks are executed before the updates, when the blocktype for updates is specified to be executed.
Serializing / deserializing UnitOfWork2 instances to/from XML
Almost all low level query api elements are serializable to/from XML to support an Xml serializable UnitofWork2 class. All api elements are xml serializable except dynamic relation and derived table.
Xml Serialization mechanism
As a unit of work requires not to have entities being duplicated in the XML, it was necessary to implement WriteXml/ReadXml methods on all elements to serialize. Although this means almost all low-level query API elements are serializable to XML, it's recommended to avoid interfaces which accept predicates and the like, and instead implement interfaces which expose methods with parameters. This avoids coupling of client code to how given parameters result in predicates on the service side.
All elements addable to a Unit of Work are serializable, except delegate calls. Delegate calls are not serialized as they're not deserializable from within the unit of work.