Transaction Support
What are transactions?
Transactions encapsulate multi-step processes where it is important that either all steps of a process run to successful completion, or they must not have any effect at all. The typical example is a bank transfer where money is booked out of one account into another - either both these steps must run successfully or none at all, meaning if crediting the target account fails for some reason, the debit to the source account must be rolled back as well.
Transactions have been traditionally supported by relational database management systems, but in recent years they have been more widely used in all kinds of scenarios where the same principles apply, usually to some kind of persistent storage of information. Microsoft Windows has contained the Distributed Transaction Coordinator (DTC) for a long time now, which allows to include all kinds of work done by services on Windows systems into transactions. For .NET 2, a number of new classes have been made available under the System.Transactions namespace, utilizing the DTC as well as a lightweight AppDomain-wide transaction mechanism.
What are Units of Work?
Units of Work perform an encapsulation of a process that looks pretty similar to what transactions do. There are two important differences:
-
A Unit of Work should not involve long-running (database?) transactions, as it is assumed that the work within the unit may take a long time or require multiple client requests to complete.
-
Changes within a Unit of Work should be collected automatically. The Unit of Work should be able to find out what has been done and
make modifications to the database accordingly.
Using transactions in XPO
Working with transactions in XPO is very easy. The Session class has three methods related to transaction handling, which are called BeginTransaction, CommitTransaction and RollbackTransaction. Typically these calls are used in code like this:
|
... using (Session session = new Session()) { session.BeginTransaction(); try { session.CommitTransaction(); } catch { session.RollbackTransaction(); throw; } } ...
... Using session1 as Session = New Session session1.BeginTransaction Try session1.CommitTransaction Catch session1.RollbackTransaction Throw End Try End Using ...
|
There might be additional work involved in the catch case, because even though the transaction has been rolled back, changes that have been made to existing objects have not been reverted. So in addition to rolling back the transaction, the objects that have been modified within the block should be reloaded from the database to make sure they have their original state once again. Of course this is only necessary if there's more work to be done with these same objects after the transaction has been rolled back.
The implementation of transactions in XPO 6.1 is such that transactions on the XPO layer don't correspond to transactions on the database layer. It is therefore possible to have long-running transactions in XPO without promoting concurrency issues in multi-user use cases. Only when a transaction is committed, a short database transaction is started to apply the changes.
Using Units of Work in XPO
The easiest usage example for a Unit of Work looks like this:
|
... using (UnitOfWork unitOfWork = new UnitOfWork()) { unitOfWork.CommitChanges(); } ...
... Using unitOfWork1 as UnitOfWork = New UnitOfWork unitOfWork1.CommitChanges End Using ...
|
In this case there's no specific rollback code, the changes are only committed to the database if the CommitChanges method is executed. The UnitOfWork class derives from Session, and so all objects that are being handled within a unit must be fetched via that unit or associated with it by means of the session constructors in the XPO base classes.
Internally, the UnitOfWork class uses a single transaction that is started automatically, committed in the CommitChanges method and rolled back when the unit is being disposed. As the class derives from Session, the "normal" transaction handling methods are also available, but their direct use may interfere with the workings of the UnitOfWork, so this shouldn't normally be done.
Nested Units of Work
Within sessions of any kind, whether "normal" Session instances or UnitOfWork instances, nested Units of Work can be created. Changes that are being made within these nested units will be merged back into the outer session if the nested unit is committed, or discarded otherwise. As the nested unit works with clones of the data, changes that are being done within it do not immediately effect the outer session.
Consider the following code example. It shows how an object is handled in main and nested Units of Work and how changes are merged back from a nested unit into an outer one.
|
...
using (Session session = new Session( )) { new Person(session, "Willy Watt", 42).Save( ); }
using (UnitOfWork unitOfWork = new UnitOfWork( )) { Person willy = unitOfWork.FindObject<Person>(new BinaryOperator("Name", "Willy Watt"));
Debug.Assert(willy.Age == 42, "Wrong age!");
using (UnitOfWork nestedUnit = unitOfWork.BeginNestedUnitOfWork( )) { Person nestedWilly = nestedUnit.FindObject<Person>(new BinaryOperator("Name", "Willy Watt")); nestedWilly.Age = 52; }
Debug.Assert(willy.Age == 42, "Wrong age!");
using (UnitOfWork nestedUnit = unitOfWork.BeginNestedUnitOfWork( )) { Person nestedWilly = nestedUnit.FindObject<Person>(new BinaryOperator("Name", "Willy Watt")); nestedWilly.Age = 62; nestedUnit.CommitChanges( ); }
Debug.Assert(willy.Age == 62, "Wrong age!"); } ...
...
Using session1 as Session = New Session New Person(session1, "Willy Watt", 42).Save End Using
Using unitOfWork1 as UnitOfWork = New UnitOfWork Dim willy as Person = unitOfWork1.FindObject(Of Person)(New BinaryOperator("Name", "Willy Watt"))
Debug.Assert((willy.Age = 42), "Wrong age!")
Using nestedUnit1 as UnitOfWork = unitOfWork1.BeginNestedUnitOfWork Dim nestedWilly1 as Person = nestedUnit1.FindObject(Of Person)(New BinaryOperator("Name", "Willy Watt")) nestedWilly1.Age = 52 End Using
Debug.Assert((willy.Age = 42), "Wrong age!")
Using nestedUnit2 as UnitOfWork = unitOfWork1.BeginNestedUnitOfWork Dim nestedWilly2 as Person = nestedUnit2.FindObject(Of Person)(New BinaryOperator("Name", "Willy Watt")) nestedWilly2.Age = 62 nestedUnit2.CommitChanges End Using
Debug.Assert((willy.Age = 62), "Wrong age!") End Using ...
|
Automatic collection of changes
If you read the last example carefully, you might have noticed that in the last nested block, the Age property is changed an the changes are committed, but the nestedWilly object is really never saved. The fact that the change to the property is being recognized although it's not explicitely saved is one of the features of the Unit of Work concept. For technical reasons though, it is necessary to code property setters in a specific way to make this work. This is what the code for the Age property in the example looks like:
|
private int age; public int Age { get { return age; } set { if (age != value) { int oldAge = age; age = value; OnChanged("Age", oldAge, age); } } }
Private age As Integer Public Property Age As Integer Get Return Me.age End Get Set(ByVal value As Integer) If (age <> value) Then Dim oldAge As Integer = age age = value OnChanged("Age", oldAge, age) End If End Set End Property
|
You should always implement properties this way if you want changes to be recognized automatically in Units of Work.
Isolation - what can be seen?
When a transaction is running and there are uncommitted changes in it - remember that transactions are also the basis of Units of Work and they don't correspond to database transactions - a newly constructed XPCollection will not normally be able to "see" the changes that have been made in the current transaction. This is because the collection fetches its content from the database, but the changes in question have not yet been written there. This is the default behaviour, but we have introduced a flag called PersistentCriteriaEvaluationBehavior that allows you to see a merged state of the data, where changes from the current running transaction are already included.
Please consider this code. The default collection - which uses the enum value PersistentCriteriaEvaluationBehavior.BeforeTransaction internally - does not see the newly created object, while the collection with the specified behavior PersistentCriteriaEvaluationBehavior.InTransaction finds both the old and the new object.
|
using (Session session = new Session( )) { session.ClearDatabase( ); new Person(session, "Willy Watt").Save(); }
using (UnitOfWork unitOfWork = new UnitOfWork( )) { new Person(unitOfWork, "Billy Bott").Save( );
XPCollection<Person> people = new XPCollection<Person>(unitOfWork); Debug.Assert(people.Count == 1, "Wrong count");
people = new XPCollection<Person>(PersistentCriteriaEvaluationBehavior.InTransaction, unitOfWork, null); Debug.Assert(people.Count == 2, "Wrong count"); }
Using session1 as Session = New Session session1.ClearDatabase New Person(session1, "Willy Watt").Save End Using
Using unitOfWork1 as UnitOfWork = New UnitOfWork New Person(unitOfWork1, "Billy Bott").Save
Dim people as XPCollection(Of Person) = New XPCollection(Of Person)(unitOfWork1) Debug.Assert((people.Count = 1), "Wrong count")
people = New XPCollection(Of Person)(PersistentCriteriaEvaluationBehavior.InTransaction, unitOfWork1, Nothing) Debug.Assert((people.Count = 2), "Wrong count") End Using
|
To learn more about XPO, please write to us at: info@devexpress.com.
To order your copy, visit our online order page.
|