ORM Library

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:

C#
VB.NET
...
using (Session session = new Session()) {
    session.BeginTransaction();
    try {
        // Create, update or delete objects
        session.CommitTransaction();
    }
    catch {
        session.RollbackTransaction();
        throw;
    }
}
...

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:

C#
VB.NET
...
using (UnitOfWork unitOfWork = new UnitOfWork()) {
    // Create, update or delete objects
    unitOfWork.CommitChanges();
}
...

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.

C#
VB.NET
...
// Create a Person object for the test
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"));

    // willy.Age will return 42 at this point
    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;
        // We leave the nestedUnit without committing its changes
    }

    // willy.Age will still return 42 here
    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( );
    }

    // Now willy.Age will return 62
    Debug.Assert(willy.Age == 62, "Wrong age!");
}
...

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:

C#
VB.NET
private int age;
public int Age {
    get { return age; }
    set {
        if (age != value) {
            int oldAge = age;
            age = value;
            OnChanged("Age", oldAge, age);
        }
    }
}

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.

C#
VB.NET
// First clear the database and create one object
using (Session session = new Session( )) {
    session.ClearDatabase( );
    new Person(session, "Willy Watt").Save();
}

using (UnitOfWork unitOfWork = new UnitOfWork( )) {
    // Create a second object
    new Person(unitOfWork, "Billy Bott").Save( );

    XPCollection<Person> people = new XPCollection<Person>(unitOfWork);
    // This collection doesn't see the new object
    Debug.Assert(people.Count == 1, "Wrong count");

    people = new XPCollection<Person>(PersistentCriteriaEvaluationBehavior.InTransaction, unitOfWork, null);
    // This collection sees both objects
    Debug.Assert(people.Count == 2, "Wrong count");
}

To learn more about XPO, please write to us at: info@devexpress.com.
To order your copy, visit our online order page.

More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 8:30am and 5:00pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.