Data Context

Introduction

The data context / session object keeps track of all objects you load, new objects you add, existing objects you delete and changes you make to any of these objects during a HTTP request. Only when flushing the session will the database be altered (that is: will the necessary create, update and delete statements be sent to the database). Since we use transactions you must still commit the data context for the changes to be persisted.

The data context lives for the lifetime of a HTTP request and any changes not committed will not be persisted. Therefore you don't have to explicitly call Rollback to revert any changes at the end of the request.

Begin

This will start the transaction.

A transaction is automatically started once the request begins but if you need multiple commits then you will need to call this method to restart the transaction.

Commit

This will commit all pending changes to the database and end the transaction.

If a pending change exists on a database table then the table is locked. Calling this will release the lock. This is useful for long running tasks. You must make sure you call Begin to restart the transaction if you still wish to use the data context.

You may also wish to call Commit more than once if you need to make sure a record/log is inserted after a payment has been made in case the script is abruptly ended e.g. because of an application pool recycle.

Note: Calling Commit with FlushMode.Commit will automically flush the changes, therefore you don’t need to call Flush and Commit together.

Flush

You should rarely have to call this method. However, it can be useful when you need to make sure a change is flushed to the database e.g. updating an identity field. Please note that the changes will not be committed until you call Commit.

When running long running tasks it is recommended to periodically flush the changes. When importing you can then clear the session to avoid the session bloating since inserts are automatically added to the session level cache. You don't need to clear the session when updating data.

An insert with an identity column is automatically flushed (regardless of the session's flush mode) to the database since we use an identity generator which gets the id after the entity is stored in the database).

See https://weblogs.asp.net/ricardoperes/nhibernate-pitfalls-flush-mode for more information about the flush mode. By default, we set it to “Commit”. This is better for performance, but it may require you to do a manual flush to guarantee your data is pushed before later querying it in the same transaction.

Clear

This allows you to clear the non-persistent (first-level cache – see below for more information) which only exists for the lifetime of the request. This also removes any pending changes which have not been flushed (see above about inserts which are automatically flushed). This is useful when executing long running tasks to avoid the session bloating.

An alternative approach is to use a stateless session which doesn’t cache anything. However, this is discouraged as it doesn’t support lazy loading or cascading.

Note: Calling Flush or Commit does not Clear the session so they should be used together.

Note: This will also disconnect any objects retrieved from the data context. Therefore, if you make changes to an object which has been cleared since it was retrieved then the changes won’t be persisted when you call Commit. You'll need to either call Update feeding in the updated entity to make sure the update is manually triggered or Merge which updates the database with the values from a detached object. Update performs better than Merge, however Update can lead to the error “a different object with the same identifier value was already associated with the session” being thrown. It is recommended not to use Clear when performing batch updates.

Evict

This allows you to remove the non-persistent (first-level cache – see below for more information) for a particular entity.

Evict Queries

This allows you to remove the persistent (second-level cache – see below for more information). You should do this when modifying data outside of the data context e.g. in a stored procedure or trigger.

Merge

Updates the database with the values from a detached object. This does the opposite of Refresh. Click here for more information.

Refresh

Updates an object if it has been modified outside of the data context.

Database Provider

This allows you to execute an SQL query or stored procedure outside of the NHibernate session. If data is changed, then since it is outside of the data context you may wish to call EvictQueries against the data context to clear the persistent cache (second-level cache – see below for more information).

Caching

First-Level Caching

This is enabled by default per session (request). Therefore, if the same entity is retrieved twice then it will only hit the database once. It is also known as the session level cache.

Second-Level Caching

This needs to be enabled per entity in the mapping. There are 3 types of cache available:

  • ReadWrite – This gives the worst performance but is needed when data changes.
  • NonStrictReadWrite – This performs slightly better than read write but should only be used if it is extremely unlikely that two transactions would try to update the same item simultaneously.
  • ReadOnly – This is if the data never changes. This performs best.

When enabled it will cache an entity for the duration of the session factory (application).

Query Caching

The following query will do a join against the role but will still hit the database once:

var query = await _dataContext.Repository<User>().All().Fetch(u => u.Role).ToListAsync();

If you add caching to the User entity (via the Cache method in the mapping file) then the above query would still hit the database once. You need to enable query caching to cache the query by feeding in true to the “All” method. Here’s an example:

var query = await _dataContext.Repository<User>().All(true).Fetch(u => u.Role).ToListAsync();

This will still hit the database every time it gets the role information even though you have tried to fetch the role. This is because the Role is not cached. To fix this problem enable caching against the Role entity and now no queries will be executed once the data is added to the cache.

If you retrieve a single record from the database (using Get<T>) then it will also retrieve from the second-level cached (depending on whether you have enabled it against the User entity.

Fetching

As you can see above you can use the Fetch and/or the FetchMany methods to fetch data upfront in a single query. This works when using the “All” method. If you use Get<T> you can still fetch associated data by specifying Fetch.Join() against the property you wish to fetch in the mapping file. This has no effect on queries retrieved by saying All.

You can only use FetchMany once per query and you should not use FetchMany when doing a Take (when doing paging) as it will limit the results in the FetchMany collection to the number of results to take. For example, say you have the following query (where there are 5 galleries, each with 10 images):

var galleries = await _dataContext.Repository<Gallery>().All().FetchMany(g => g.Images).Take(2).ToListAsync();

This will return 1 gallery, but 2 images as it simply retrieves the first 2 records from the cartesian product. To solve this, you should use future queries which will execute the queries individually so you don't get a cartesian product. Here's how you would correct the above example:

var query = _dataContext.Repository<Gallery>().All();
query.FetchMany(g => g.Images).ToFuture();
var galleries = await query.Take(2).ToFuture().GetEnumerableAsync();

Note: Future queries offer an additional advantage by addressing the limitation of having only one FetchMany per query, since you can add as many future queries with FetchMany as you like.

One to Many Relationships

To enable caching against a one to many relationship you need to specify your type of cache against the property in the parent collection (not the entity itself). You should also say FetchMany which makes sure the join is made before the database is hit.

You should only do this for properties which you directly add/delete the data from. For example:

Say you have a comments collection against an article. If you said article.Comments.Add(…) then the Comments cache would be updated. But if you said await _dataContext.Repository<Comment>.InsertOrUpdateAsync(…) then the Comments cache would not.

Read-Only

Marking an entity as read-only means changes are not tracked.

As mentioned previously, any time you retrieve an entity it is automatically stored in the first-level cache. If you was to do a read-only query, e.g.:

var query = await _dataContext.Repository<User>().All(readOnly: true).ToListAsync();

The entities would be stored in the cache as read-only. Therefore any subsequent requests to the same entity would also be marked as read-only.

Note: You can toggle the read-only state of an entity by calling the “SetReadOnly” method against the data context.

Database Locks

When a change is flushed but not committed to the database it will lock the record. In the case of an update you won’t be able to select it, but you can select other non-modified records in the same table. You also won’t be able to do a select which would return the modified data.

For example, if you said the following after adding/updating and/or deleting some data in the "Users" table then it wouldn’t be able to finish executing until the data is committed and the lock is released:

await _dataContext.Repository<User>().All().CountAsync();

However, you can change the above to the following to query the table but ignore the locks:

await _dataContext.Repository<User>().All().WithNoLock().CountAsync();

Importing Data in Bulk

When importing data in bulk you should execute either one of the following options after so many iterations to prevent the problems shown.

 Database LocksMemory BloatCan Rollback
1. Commit(true)NoYesNo
2. Flush() then Clear()YesNoYes
3. Commit(true) then Clear()NoNoNo

Option 2 is preferred, although it will lock the table, you will still be able to roll the changes back if there is a problem. You also won’t suffer from memory bloat. If you use Commit instead of Flush (to prevent locking), then you should validate the data upfront, so you don’t have the need to roll the changes back.

Updating Data in Bulk

There is no need to Clear the data context when performing bulk updates. Doing so would cause the data to be detached from the data context. This would result in you having to manually call either Update/Merge. See above for more information. However you could call Evict to manually remove a single entity from the session's cache once you have flushed any changes.

Additional Info

Soft Deletes

If your entity implements ISoftDeletable then when you delete the entity against the data context it will simply mark it as deleted and not physically remove it from the database.

If you add the following to your mapping for the entity it will automatically filter out the deleted items:

Where("IsDeleted = 0")

For example, when you query the entity by saying either of the following it will automatically add WHERE IsDeleted = 0 to your query:

await _dataContext.Repository<Product>().GetAsync(1);
await _dataContext.Repository<Product>().All().ToListAsync();

However, it won't add the filter when retrieving a collection of the entity or when retrieving an entity which references your entity, for example the following will still retrieve the deleted Products:

(await _dataContext.Repository<ProductListing>().GetAsync(1)).Products;
await _dataContext.Repository<ProductFieldValue>().All().Where(fv => fv.Product.Id == 1).ToListAsync(); // This will still get the field values even if the filtered Product has been deleted.

Please note that single entities (mapped as ManyToOne) which are lazy loaded will also filter out the deleted items. If you don't want this to happen, you should pre-fetch the data in the query, e.g. FetchMany(o => o.AllLines).ThenFetch(l => l.Product).

Resources