Two assertions:
- I like the dirty check of NHibernate because I can work without worry about explicit updates.
- I don’t like the dirty check of NHibernate because I can’t have full control about updates and the dirty-check, in my application, is slow.
Both true ? Can we implements something to prevent Auto-Dirty-Check on flush ?
Domain
Test
Part of the configuration
<property name="generate_statistics">true</property>
I’m using NHibernate SessionFactory statistics to check some operation.
Populating DB
public void FillDb()
{
sessionFactory.EncloseInTransaction(session =>
{
for (int i = 0; i < 100; i++)
{
var reptileFamily = ReptileFamilyBuilder
.StartRecording()
.WithChildren(2)
.Build();
session.Save(ReptilesfamilyEntityName, reptileFamily);
}
for (int i = 0; i < 100; i++)
{
var humanFamily = HumanFamilyBuilder
.StartRecording()
.WithChildren(1)
.Build();
session.Save(HumanfamilyEntityName, humanFamily);
}
});
}
In a transaction I’m creating 100 Family<Reptile> and 100 Family<Human>. Each Family<Reptile> has a father, a mother and two children (total 5 entities). Each Family<Human> has a father, a mother and one children (total 4 entities). The DB will have 900 entities states (the Family is mapped to use all cascade).
The test
public void ShouldNotAutoUpdate()
{
FillDb();
using (ISession s = sessionFactory.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
var reptiles = s.CreateQuery("from ReptilesFamily")
.Future<Family<Reptile>>();
var humans = s.CreateQuery("from HumanFamily")
.Future<Family<Human>>();
ModifyAll(reptiles);
ModifyAll(humans);
sessionFactory.Statistics.Clear();
s.Update(ReptilesfamilyEntityName, reptiles.First());
s.Update(HumanfamilyEntityName, humans.First());
tx.Commit();
}
sessionFactory.Statistics.EntityUpdateCount
.Should().Be.Equal(7);
CleanDb();
}
After populate the DB I’m loading and modifying all instances of Human and Reptile (that mean 400 entities of Reptile and 300 entities of Human). The result is that I have 900 entities loaded and 700 modified in a session.
In the two session.Update I’m calling explicitly the update only for the first Family<Reptile> and the first Family<Human> (that mean only for 7 entities).
The test assertion is:
sessionFactory.Statistics.EntityUpdateCount
.Should().Be.Equal(7);
The summary is that even if I have 700 modified entities, NHibernate should update only 7 entities because I call explicitly Update only for two families.
How change the default behavior
If you are familiar with NH2.0.0 and above you can imagine which will be the place where look… yes, Events/Listeners.
As first the configuration where you can see which events I’m using and which listeners and in which order will be executed.
<event type="delete">
<listener
class="DisableAutoDirtyCheck.PreDeleteEventListener, DisableAutoDirtyCheck"/>
<listener
class="NHibernate.Event.Default.DefaultDeleteEventListener, NHibernate"/>
</event>
<event type="update">
<listener
class="DisableAutoDirtyCheck.PreUpdateEventListener, DisableAutoDirtyCheck"/>
<listener
class="NHibernate.Event.Default.DefaultUpdateEventListener, NHibernate"/>
</event>
<listener
class="DisableAutoDirtyCheck.PostLoadEventListener, DisableAutoDirtyCheck"
type="post-load"/>
Tricks
The real Dirty-Check happen in the DefaultFlushEntityEventListener using the session state. All entities loaded, in what is commonly named session-cache, are loaded in the Session.PersistenceContext. To be very short the PersistenceContext is a set of EntityEntry. An EntityEntry is the responsible to maintain the state and the Status of an entity.
The real trick behind all this matter is this extension:
public static class Extensions
{
private static readonly FieldInfo statusFieldInfo =
typeof (EntityEntry).GetField("status",BindingFlags.NonPublic | BindingFlags.Instance);
public static void BackSetStatus(this EntityEntry entry, Status status)
{
statusFieldInfo.SetValue(entry, status);
}
}
Listeners implementation
public class PostLoadEventListener : IPostLoadEventListener
{
public void OnPostLoad(PostLoadEvent @event)
{
EntityEntry entry = @event.Session.PersistenceContext.GetEntry(@event.Entity);
entry.BackSetStatus(Status.ReadOnly);
}
}
After load an entity, the instance is marked as ReadOnly but maintaining the loaded-state (maintain the loaded state is the reason to use the above trick).
public class PreUpdateEventListener : ISaveOrUpdateEventListener
{
public static readonly CascadingAction ResetReadOnly = new ResetReadOnlyCascadeAction();
public void OnSaveOrUpdate(SaveOrUpdateEvent @event)
{
var session = @event.Session;
EntityEntry entry = session.PersistenceContext.GetEntry(@event.Entity);
if (entry != null && entry.Persister.IsMutable && entry.Status == Status.ReadOnly)
{
entry.BackSetStatus(Status.Loaded);
CascadeOnUpdate(@event, entry.Persister, @event.Entity);
}
}
private static void CascadeOnUpdate(SaveOrUpdateEvent @event, IEntityPersister persister, object entity)
{
IEventSource source = @event.Session;
source.PersistenceContext.IncrementCascadeLevel();
try
{
new Cascade(ResetReadOnly, CascadePoint.BeforeFlush, source).CascadeOn(persister, entity);
}
finally
{
source.PersistenceContext.DecrementCascadeLevel();
}
}
}
When an entity is explicitly updated, before execute the default behavior I’m restoring the Status of the loaded entity (obviously for all the entities loaded an involved in cascade actions).
Conclusion
Can we have full control over NHibernate’s updates ? Yes, we can!! ;-)
Code available here.