Transactions

Transactional transparency

DataObjects.Net is built to simplify writing business code – a code written on high-level language (e.g. C#) and operating on application server.

This brings some new effects in comparison to the case developers faced earlier, when similar code was written in stored procedures:

  • There is an ORM transforming relational data to high-level language objects. Normally presence of ORM implies significant differences in behavior: e.g. if you remove the row in database, this happens immediately – and it’s very convenient to rely on sequence of operations there. But many ORM tools delay actual operations until explicit invocation of Session.Flush() or similar method – so basically, you can’t be sure the state you see is the same as the state in RDBMS. May be the closest analogue of this is WYSIWYG principal, and here it’s broken.
  • Frequently application server is communicating with RDBMS over network, so developers must care about reducing the chattiness between RDBMS and application server.

DataObjects.Net addresses both of these issues:

  • Transparent persistence and transactional transparency ensure you’re working with entities as if they’re “directly connected” to the underlying database objects:

    • On any reads, you get exactly the same values that must be retrieved from the database right directly. So if you just read something, direct SQL query executed on the same connection & transaction, and retrieving the same data directly, would return exactly the same value.
    • Any cached changes are persisted before any data retrievals.
    • Transaction rollbacks are immediately reflected in state. There is no necessity to update anything manually.
    • Any cached state is considered as stale on crossing the transaction boundary. For simplicity, you can imply DataObjects.Net simply invalidates it.
    • And so on. The final goal is to simulate the “direct connectivity”, but cache as much as possible without breaking this to reduce the chattiness.

    So DataObjects.Net provides “full WYSIWYG” here.

  • Generalized batching, future queries and prefetch API allow you to dramatically reduce the chattiness with almost zero efforts.

    Note

    Transparent persistence and transactional transparency allows developer to be sure that he always deal with actual state: the state he sees is absolutely the same as direct SQL queries performed on the same connection & transaction would show.

For example, if you’re fetching some entity in transaction TA and trying to get its property value in transaction TB, by default its value would be re-fetched there. “By default” indicates there are ways to make DataObjects.Net behave differently, but you must do this explicitly.

Note

As a consequence, DataObjects.Net requires any data access operations to be executed inside logical transactions. “Logical” here means that these transactions define isolation boundaries just for DataObjects.Net.

Managing transactions

To open a transaction, you should call session.OpenTransaction(...) method (it has several overloads):

using (var session = domain.OpenSession()) {
  using (var transactionScope = session.OpenTransaction()) {

    var person = new Person();
    person.Name = "Barack Obama";

    transactionScope.Complete();
  }
}

Asynchronous opening is also possible:

await using (var session = await domain.OpenSessionAsync()) {
  await using (var transactionScope = await session.OpenTransactionAsync()) {

    var person = new Person();
    person.Name = "Barack Obama";

    transactionScope.Complete();
  }
}

The simplest form of this method opens a transaction in active session and returns its transaction scope – an IDisposable object used to committing or rolling back the transaction.

  • To commit the transaction, call transactionScope.Complete() method and dispose the transaction scope.

  • To rollback the transaction, just dispose the transaction scope without calling Complete() method.

    Note

    In example above we open a transaction inside using block, so transaction scope returned by Open() method will be disposed on leaving this block. If code inside using block throws an exception, Complete() method won’t be invoked. So our transaction will be committed only if code inside it is executed successfully.

If session.OpenTransaction(...) method is invoked, but active session already has associated transaction, it does nothing and return so-called void scope. Such a scope does nothing on Complete() method call and its disposal as well.

You can also open a transaction with specified isolation level: session.OpenTransaction(IsolationLevel isolationLevel).

Nested transactions

Use session.OpenTransaction(TransactionOpenMode.New) to open a new transaction. If there is no outermost transaction in the active Session, the outermost transaction will be opened; otherwise a nested one will be opened.

Nesting level is limited only by underlying RDBMS.

var domain = BuildDomain();

using (var session = domain.OpenSession()) {
  using (var transactionScope = session.OpenTransaction()) {

    var john = new User {
      Name = "john doe"
    };

    // Opening a nested transaction
    using (var nestedScope = session.OpenTransaction(TransactionOpenMode.New)) {
      john.Name = "john smith";
      // Omitting nestedScope.Complete(), so nested transaction will be rolled back
    }

    Assert.AreEqual("john doe", john.Name);

    // Marking the transaction scope as completed to commit the outermost transaction
    transactionScope.Complete();
  }
}