Transactions and unit of work

Most business operations result in addition/removal of, updates to database records. A business operation is considered successful only when all constituent database operations are committed to database successfully, among other things. Failure of even a single database operation signals the failure of the whole business operation. In this situation, all other successful database operations need to be reverted back. Most relational databases offer ability to bundle multiple database operations as one unit called transaction. Relational databases offer a guarantee that either all operations inside a transaction succeed or they all fail, leaving the data in a consistent state. This behavior is also described using ACID properties which stand for Atomic, Consistency, Isolation, and Durability. These properties guarantee that database transactions are processed in a reliable manner. Let's briefly understand what behavior these properties define:

  • Atomicity: All operations in a transaction are performed as one atomic unit. Either all of them succeed or all fail.
  • Consistency: Database remains in a consistent state before the start of the transaction and after the end of the transaction, irrespective of whether transaction succeeds or fails.
  • Isolation: During a transaction, no other database operation can access the data operated upon by the transaction. In reality, databases offer different levels of isolation to let applications define what level of access other database operations can have while the transaction is going on.
  • Durability: Once the user has been notified of the success of the transaction then it cannot be undone. All the changes are persisted and are not lost even if the database system subsequently fails.

Unit of work is a design pattern which assists with reliably inserting, updating, and deleting records in database as one unit of work, in accordance with the business operations being carried out. NHibernate supports unit of work pattern through its own implementation of transaction. To make use of this, you first need to tell session object to begin a transaction by calling the BeginTransaction method on session object. Once a transaction is started, any database operation performed using the session object is recorded but is not actually sent to database. When you are finished with your business operation, you can call the Commit method on the transaction to tell NHibernate that you do not intend to carry out any more database operations as part of this business operation and NHibernate should send all the database operations to the database server as one unit. Following is a simplified version of how this code would look:

using (var session = config.OpenSession())
{
  using (var transaction = session.BeginTransaction())
  {
    try
    {
      //Database operations here
      transaction.Commit();
    }
    catch (Exception ex)
    {
      transaction.Rollback();
      throw;
    }

  }
}

Most important bits in the preceding piece of code are beginning the transaction and running database operations inside of a try…catch block. If there is any error while database operations are being carried out, an exception would be thrown which is caught in the catch block. Here, the transaction is rolled back, resulting in reverting any state changes that were applied to data in database and removing all database operations recorded in the session object. If database operation finished without errors then call to Commit at the end of the try block would make sure that all the database operations are sent to database as one unit. They all either update data in the database or throw an error and are rolled back.

Tip

Also keep in mind that if the ISession or ITransaction instance is disposed off without committing the transaction then the transaction would be rolled back. This behavior may lead to subtle bugs if you have mistakenly forgotten to commit a transaction.

Remember that NHibernate transaction ultimately results in a database transaction but it is not the same as a database transaction. SQL to start a database transaction is sent to database as soon as ITransaction.BeginTransaction is executed. The lifetime of database transaction is controlled by NHibernate transaction.

Note

I said previously that database operations are only recorded by session object but are not actually sent to database till call to Commit happens. This is not entirely true. Commit internally calls the Flush method on session. The Flush method is responsible for generating and sending SQL commands to database. You can call Flush yourself from within a transaction and all your changes would be sent to database, though this is not a recommended practice.

Explicit and implicit transactions

Transactions, as we discussed previously, are called explicit transaction. This is because they are explicitly started by our code. Use of explicit transactions is a very strongly recommended practice, no matter what kind of database operations you are carrying out. If you do not make use of explicit NHibernate transactions then database server implicitly makes use of transactions for every database command that it executes. These transactions are referred to as implicit transactions. If a business operation results in multiple SQL commands then ideally these should all be executed as one unit as we just discussed. Explicit NHibernate transactions let us do that. With implicit transactions, every SQL command in the batch would be executed inside a different transaction. If one of the SQL commands fails then there is no way to recover the previously committed transactions. Such behavior makes it difficult to implement a reliable unit of work pattern. This not only leaves your data inconsistent in case of errors, but also adds to the execution time as there are more number of transactions started and committed. The overall effect of this is reduced performance and inconsistent data if anything goes wrong.

Entity states and transactions may not make much sense to you at this point. But as we learn more about using NHibernate persistence API to store our entities in database, these concepts would start becoming more relevant. Let's understand what flush mode is before we look at a simple use of NHibernate persistence API to save an entity.

Flush modes

When a transient entity instance is presented to NHibernate in order to persist to database, it is not saved in the database immediately. Rather, NHibernate keeps this instances into a special memory map called identity map maintained by the session object. Same happens when a persistent entity instance is modified. NHibernate keeps building the identity map as application code adds more and more transient entity instances to it or modifies persistent entities. At various times during the life cycle of session object, NHibernate would synchronize the entity instances present in the identity map with the database. This process is also called flushing. The Flush method on ISession implements this behavior. The times at which entities are flushed is determined by different flush modes. Flush modes have a bearing on how the application code should be written to ensure the entities are synchronized with the database. So it is important to understand different flush modes. NHibernate defines following four flush modes:

  • Never: Changes are never flushed automatically by NHibernate. We must call ISession.Flush manually before closing the session. Failing to call ISession.Flush would result in loss of data.
  • Commit: This flush mode is to be used in conjunction with a transaction. With flush mode set to Commit, the changes are flushed as soon as the transaction is committed and there is no need to call ISession.Flush manually.
  • Always: When application code saves an entity, NHibernate can choose to perform the action later. But when application queries for data, the query has to be executed immediately to return the results to the application code. Whenever NHibernate hits the database to execute the query, it has an opportunity to synchronize all the new or modified entities to database. Flush mode Auto enables this behavior. There is no need to call ISession.Flush manually with flush mode set to Always.
  • Auto: This mode can be considered as a more efficient version of Always. Similar to Always, session is flushed before executing queries, but unlike Always, not before executing every query. If session holds entities which when synchronized with database, may have an impact on the result of the query, then NHibernate would flush the session, otherwise it would not. This prevents the query execution from returning stale data. For instance, if session holds a modified version of the Employee instance and we are querying for Employee, then it is possible that the instance held within session may be part of the result of the query and entities in the session would be flushed.

Also, remember that modes Always and Auto, though intelligent, are doing more checks before every query execution to determine whether session should be flushed or not. This adds some overhead to the execution of the query. We cannot say that the overhead is significant as it may vary from application to application.

Note

When ITransaction.Commit is called, session is always flushed unless the flush mode is set to Never. From the above description of flush modes, it is easy to assume that commit of a transaction results in session being flushed only if flush mode is set to Commit. That is not true. Setting flush mode to Commit would stop the flush mode Auto or Always from coming into effect, but setting flush mode to Auto or Always would not stop ITransaction.Commit from flushing the session. In the previous unit of work implementation we went with the default value of flush mode which is Auto, which works. But we could also have gone with Commit.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset