NHibernate - Getting up and running

Persisting objects back to the database with NHibernate

Index

Introduction to NHibernate
Simple Queries on a single table with NHibernate
Configuring Child Collections with NHibernate
Overriding Equals, GetHashCode and Database Design with NHibernate
Querying Collections Efficiently with NHibernate
Persisting objects back to the database with NHibernate

Introduction

In most instances, saving an object to the database is as simple as calling Session.Save(obj), if this statement is within a transaction, the object will be persisted back to the database on the commit.

Saving a new instance of an object to the database

        [Fact]
        public void CanInsertNewEmployee() {

            int employeeId = 0;

            Assert.DoesNotThrow(() => {
                using (var tx = Session.BeginTransaction()) {
                    var e = new Employee() {
                        BirthDate = DateTime.Now,
                        HireDate = DateTime.Now
                    };

                    Session.Save(e);
                    tx.Commit();

                    employeeId = e.EmployeeId;
                    Assert.NotEqual(0, employeeId); 
                }

                using (var tx = Session.BeginTransaction()) {
                    Session.Delete(Session.Load(employeeId));
                    tx.Commit();
                }
            });
        }

In cases where the object has not be disconnected* from the session, updating objects is even easier, simply make the changes to the object and commit the transaction (see test below)

*In some high concurrency applications, the object may be sent to the view for updating and the session disconnected. When the changes from the UI are posted back, the object has to be "reconnected" which requires some additional configuration (see Disconnected Objects below)

Updating an existing object to the database

        [Fact]
        public void SimpleUpdate() {

            string fakeRegion = DateTime.Now.Second.ToString();

            using (var tx = Session.BeginTransaction()) {
                var employee = Session.Load<Employee>(5);
                employee.Region = fakeRegion;   // Forces load from database
                tx.Commit();                    // Automatically flushes changes
            }
        }

Flushing the changes to the database

NHibernate will determine when to flush the changed objects back to the database, however this can be influenced in code. Flushes will occur

  • from some invocations of Find() or Enumerable()
  • from NHibernate.ITransaction.Commit()
  • from ISession.Flush()

If all statements are wrapped in a transaction (as recommended) you can accurately predict when the objects will be flushed.

Note: If the ISession throws an exception you should immediately rollback the transaction, call ISession.Close() and discard the ISession instance.

Recommended pattern for updating objects in an application

Using MVC as an example, the recommended pattern for updating an object is to

  1. Validate the view model posted back
  2. Load the domain object from the database
  3. Update the domain object with changes from the view model
  4. Flush the changes back to the database

You should never expose your domain (NHibernate) objects to your views (or client) but rather use a bespoke view model. View models often combined and flatten many objects and present the data in a usable way to the view e.g. for a dropdown list an IList<SelectListItem>. This also ensures that there is no unintentional processing in the view (e.g. unexpected lazy loading)

The method above requires an extra select (rather than just an update) but this is acceptable overhead in most instances.

The diagram below illustrates the mapping required between the domain object and the view model.

Using NHibernate with MVC

Many people disagree with having controllers with NHibernate exposed, but prefer to abstract the API into a repository pattern or similar, injecting it in. There's probably 100's of website's and conversations on this one, but Im siding with Oren Eini, you can make up your own mind...

Disconnected Objects

In high concurrency applications objects may become disconnected from the Session from which they where loaded when be presented to the view (or user interface). When the updated object is returned, NHibernate needs to know if its a new or existing object, if its existing... has it since been changed?

Determining if the object is a new instance

Within the id element of a class, the attribute unsaved-value instructs NHibernate of the default value of a new object. More often than not this will be zero (if you're using int as a key column). NHibernate can then distinguish between new and existing objects. If unsaved-value is not specified for a class, NHibernate will attempt to guess it by creating an instance of the class using the no-argument constructor and reading the property value from the instance.

NHibernate - the unsaved-value attribute of the Id Element

Determining if the object is stale

NHibernate has a number of ways of managing concurrency, probably the easiest one to implement is the version element in the mapping file. Each time an object is updated, the version is automatically incremented. Before saving an object, the version is checked to see if its still the same, if not a StaleObjectStateException is thrown.

NHibernate - Version element of a mapping file for managing concurrency

The test below demonstrates this

        [Fact]
        public void DifferentUsersUpdatingSameVersionedObjectThrows() {
            int employeeId = 5;
            Employee employee = null;
            string fakeRegion = DateTime.Now.Second.ToString();

            var factory = Session.SessionFactory;

            Assert.Throws<NHibernate.StaleObjectStateException>(() => {

                // user1: get employee and display in UI
                var session1 = Session; 
                using (var tx = session1.BeginTransaction()) {
                    employee = Session.Get<Employee>(employeeId);
                    tx.Commit();
                }
                session1.Close();
                session1.Dispose();

                // user2: update employee
                Task.Factory.StartNew((x) => {
                    var sessionT = (x as ISessionFactory).OpenSession();

                    using (var tx = sessionT.BeginTransaction()) {
                        sessionT.Get<Employee>(5).Region = string.Concat(DateTime.Now.Second, "T");
                        tx.Commit();
                    }
                }, factory).Wait();


                // user1: update employee
                employee.Region = fakeRegion;
                var session2 = factory.OpenSession();
                using (var tx = session2.BeginTransaction()) {
                    session2.SaveOrUpdate(employee);
                    tx.Commit();
                }
                session2.Close();
                session2.Dispose();
            });
        }

ISessionFactory and ISession configurations

When embedding NHibernate into an application infrastructure it's import to make note of below

An ISessionFactory is an expensive-to-create, threadsafe object intended to be shared by all application threads.

This translates to creating a single shared ISessionFactory per application, depending on the usage this could be within the globax.asax, an ActionFilterAttribute or the dreaded singleton.

An ISession is an inexpensive, non-threadsafe object that should be used once, for a single business process, and then discarded.

ISession's can be created and disposed easily, usually one would be created with postback (or callback) to the server.

Summary

The key points to take away from the above are

  • Again, always wrap your statements in a transaction
  • Objects are flushed back to the database on the commit of a transaction
  • Versioning your mapping files is recommended
  • Update and SaveOrUpdate calls are only required for disconnected objects

Source Code

git clone https://github.com/mindfulsoftware/ORM.Nhibernate.git
(version: 2f7b07d95dd473f9adf475b5fcd4d877891d4534)