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:
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.
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.
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.
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.
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.
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
.