We’ve already saw how to setup NHibernate.Envers, how to configure and how to create the schema for it (NHibernate.Envers - a quick introduction).

To query for our revision entities we need to use the IAuditReader interface, we have two ways to obtain it:

  • Using the AuditReaderFactory.Get(ISession) method.
  • Using the ISession.Auditer() extension method.

Once we have the object in place we can start querying for data. There isn’t many documentation around (here’s the only link I was able to find and it’s from the original Java project: Hibernate Envers - Easy Entity Auditing); so your best bet is to build the documentation file from the source project or to download the source code itself and look at the comments there.

As you saw in the previous article NHibernate.Envers approaches the entity version problem defining a unique master table to hold a global revision number to which attach the history of the entities that are part of a single revision.

It creates a REVINFO table and maps it over a DefaultRevisionEntity class.

In this article we’ll focus on getting information about the Revision object, this is the class that actually identifies the current revision number and revision timestamp (it can be customized as we’ll see in a next article of this series).

Let’s see each function in detail (I’ll just look at the generic version, because I hate to write typeof() everywhere):

T GetCurrentRevision<T>(bool persist)

“Gets an instance of the current revision entity, to which any entries in the audit tables will be bound. Please note the if persist is false, and no audited entities are modified in this session, then the obtained revision entity instance won't be persisted. If persist is true, the revision entity instance will always be persisted, regardless of whether audited entities are changed or not”.

This very simple test (assuming we have an empty database) will give you an idea of how it work:

// assuming an empty database
using (ISession s = _nh.SessionFactory.OpenSession())
{
	using (var tx = s.BeginTransaction())
	{
		IAuditReader auditReader = s.Auditer();
		DefaultRevisionEntity o = auditReader.GetCurrentRevision<DefaultRevisionEntity>(false);
		tx.Commit();
		Assert.IsNotNull(o);
		// it's transient
		Assert.AreEqual(0, o.Id);
	}
	using (var tx = s.BeginTransaction())
	{
		IAuditReader auditReader = s.Auditer();
		DefaultRevisionEntity o = auditReader.GetCurrentRevision<DefaultRevisionEntity>(true);
		tx.Commit();
		Assert.IsNotNull(o);
		// it's persisted
		Assert.AreEqual(1, o.Id);
	}
}

Looking at the returned class you can have information about the Revision to which your (current or next) changes will be attached to.

T FindRevision<T>(long revision)

Gets the RevisionEntity at the given revision number, if the revision doesn’t exist a ‘RevisionDoesNotExistException’ will be raised.

The following tests will shot its usage:

[Test]
public void FindRevision_DoesNotExists()
{
	// assuming an empty database
	Assert.Throws<RevisionDoesNotExistException>(() =>
	{
		using (ISession s = _nh.SessionFactory.OpenSession())
		{
			// look for a non existing revision
			using (var tx = s.BeginTransaction())
			{
				IAuditReader auditReader = s.Auditer();
				var rev = auditReader.FindRevision<DefaultRevisionEntity>(1);
				Assert.IsNotNull(rev);
				tx.Commit();
			}
		}
	});
}

[Test]
public void FindRevision_Success()
{
	// assuming an empty database
	using (ISession s = _nh.SessionFactory.OpenSession())
	{
		// create a revision
		using (var tx = s.BeginTransaction())
		{
			// create a person
			Person p = new Person("Jhon", "Doe");
			s.SaveOrUpdate(p);
			tx.Commit();
		}
		using (var tx = s.BeginTransaction())
		{
			IAuditReader auditReader = s.Auditer();
			var rev = auditReader.FindRevision<DefaultRevisionEntity>(1);
			Assert.IsNotNull(rev);
			tx.Commit();
		}
	}
}

In the first one you will get an exception when asking for a revision that does not exists, in the second you will properly get the information you requested in the form of the default revision entity class.

IDictionary<long, T> FindRevisions<T>(IEnumerable<long> revisions)

“Find a map of revisions using the revision numbers specified”. If the revisions do not exist an empty dictionary will be returned. If you ask for a revision number that does not exist, it will just not be returned in the resulting dictionary (no exception will be thrown).

The following test cover the case of requesting a non existent revision:

[Test]
public void FindRevisions_NonExistentRevision()
{
	using (ISession s = _nh.SessionFactory.OpenSession())
	{
		// create a revision
		using (var tx = s.BeginTransaction())
		{
			// create a person
			Person p = new Person("Jhon", "Doe");
			s.SaveOrUpdate(p);
			tx.Commit();
		}
		// look for a non existent revision
		using (var tx = s.BeginTransaction())
		{
			IAuditReader auditReader = s.Auditer();
			var rev = auditReader.FindRevisions<DefaultRevisionEntity>(new long[] { 1, 2 });
			Assert.IsNotNull(rev);
			Assert.AreEqual(1, rev.Count);
			Assert.AreEqual(1, rev[1].Id);
			tx.Commit();
		}
	}
}

DateTime GetRevisionDate(long revision)

“Get the date, at which a revision was created”. The precision is database and mapping specific; the default mapping uses ‘timestamp’ as its NHibernate type (this will include the milliseconds in the date and we all know the problems with the milliseconds rounding in some database engines). If you want another behavior you will need to customize the default revision entity class (more in a next post).

If the revision doesn’t exist a ‘RevisionDoesNotExistException’ will be raised.

long GetRevisionNumberForDate(DateTime date)

Gets the revision number of the highest revision which was created on or before the given date. As you can imagine this function is pretty useless if you do not know the exact date (up to the milliseconds).

If the revision doesn’t exist a ‘RevisionDoesNotExistException’ will be raised.

Enough for today, the next time we’ll take a look at more specific queries that will involve our tracked entities too.

You can get the actual same source code here, it will contain one or more tests for each of the functions reported before:


 


 

 

Related Content