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.
Hi Fabio,
ReplyDeleteNice post, and it's good to know we have deep influence into NHibernate guts.
Now, my humble opinion: why on earth a designer would like to modify in memory entities without persisting their state? I would consider this behavior a BUG.
Requiring explicit updates assumes that you always have access and references to all modified entities.
Besides, if you modify your domain model so a specific method now changes one more entity, you have to remember to explicitly update that entity calling the repo (or dao).
I would stick with the following rule: any in-memory entity, if modified, should be persisted, no matter the developer called update or not.
The only drawback I see with this is that NH could do a lot of work checking for dirty entities (say, 1000 entities in-memory and just 2 modified), but that what's supposed to do :-)
Grazie mile
You are one of the first assertion.
ReplyDeleteAs you saw there is a performance issue and a "conceptual-masturbation" issue.
As for "identity" the matter is if it is possible to change the NH default behavior to do what you want. This post is a demonstration that "Is possible".
Yes, I'm certainly with the first assertion.
ReplyDeleteThanks Fabio
1 - it would be interesting to have this feature ready-to-use in the future, switchable via configuration file
ReplyDelete2 - I prefer the default behaviour in order to discover unexpected modification
@tomb
ReplyDelete1- Only available out-side NH, probably in uNhAddIns.
2- That was the matter: somebody don't want to persist "unexpected modification".
Fabio,
ReplyDeleteI was thinking on your post in the bus just before sleeping (about 30 seconds). Please note that I appreciate you post on the NHibernete events internals, but I just can't think on any scenario where you have "unexpected modification".
Can you detail a "use case" where this situation may arise?
Grazie mille
More than a scenario is a user request. The exigence was caused in a very particular scenario where a bug, in the application, cause an unwanted change. BTW find a way to work with explicit update is not so crazy; the are a lot of application using explicit update where the auto-dirty-check is unneeded.
ReplyDeleteHi Fabio,
ReplyDeleteI would rephrase your las paragraph as "the are a lot of application using explicit update where the auto-dirty-check is UNAVAILABLE".
I still use explicit updates in many places, mainly because I didn't knew about auto-save on dirty feature.
Gracias
Hi Fabio,
ReplyDeleteThe class ResetReadOnlyCascadeAction (wich is in your svn repository) doesn't seem to compile, giving the error: The type 'Nhibernate.Engine.CascadingAction' has no constructors defined. Am I missing something?
Thank you.
I'll check. My examples, may be, is not in sync with the last NH.
ReplyDeleteSeems a nice solution , since I don't like the auto-flush on dirty objects. :)
ReplyDeleteHowever, I experience the same problem as Pedro; I also get the error 'CascadingAction has no constructors defined'.
I'm using NH 2.0.1.4000
In the source, I see that the constructor on the abstract CascadeAction class is 'internal'. Perhaps it should be set to 'protected' ?
ReplyDeleteAhhhh this solution, as many others in this site, is for NH2.1.x.
ReplyDeleteSince this solution is going to be used by somebody else I'm going to port the implementation inside uNhAddIns (that mean it will be in sync with all others stuff)
ReplyDeleteNow all is in uNhAddIns with an extension to register events:
ReplyDeleteconfiguration.RegisterDisableAutoDirtyCheckListeners();
Nice, but it seems that this doesn't work when you call 'SaveOrUpdate' on the modified entity, instead of 'Update'.
ReplyDeleteWhen you call 'Update', the dirty entity will be saved.
When you call 'SaveOrUpdate', the changes of the dirty entity will not be persisted.
Do you have to add the ResetReadOnlyEntityUpdateListener to the SaveOrUpdate listeners ?
And what with the SaveOrUpdateCopy method ?
;) good issue. I'll write a test then create the issue in the uNhAddIns issue tracker and then fix it.
ReplyDeleteThanks Frederik.
The SaveOrUpdateCopy (that is a Merge) shouldn't be a problem... btw a test will confirm it.
ReplyDeleteAll should work fine with the implementation on uNhAddIns.
ReplyDeleteThanks Frederik.
With pleasure;
ReplyDeletequestion: why should those listeners be serializable ?
The configuration is serializable.
ReplyDeleteok.
ReplyDeleteI'm trying the updated NHUsers Addin, and I'm experiencing a problem with the SetReadOnlyEntityPostLoadListener.
I lock an entity into a session, and in the SetREadOnlyEntity method, the GetEntry method for the given entity returns null, which means the BackSetStatus method throws an exception, since it wants to set a value on a 'null' field.
This comment has been removed by the author.
ReplyDeleteHi Carlos,
ReplyDeleteJust to show you an use case:
User user = userRep.Load(1);
user.Name = null;
if(Validate(user)) {
userRep.Update(user);
}
I'm using an UoW and the session is far far away to call evict.
Allowing only specified dirty objects to be flushed is just one time saver. But, how can we get around the expensive FlushEverythingToExecutions call in OnFlush (DefaultFlushEventListener) ? If you have big sessions with many objects, these calls with cost you.
ReplyDeleteIs there a feasible way around this ?
@Lone
ReplyDeleteThis article is part of the solution because NH does not need to check the state of any obj.
Sure, but the solution only covers hits on the database. NH spends half its time collecting dirty information on all session object properties before contemplating whether the object should be persisted or not. I'm looking for a defacto way of avoiding this, possibly supplying NH with the dirty information myself. Currently, I have a solution with a rewritten DefaultFlushEventListener substitute that avoids the unnessary property lookups for objects not dirty.
ReplyDeleteWith this impl NH does not spent time checking which are dirty entities.
ReplyDeleteI say it is. Try adding your own custom FlushEventListener and add this as the DefaultFlushEventListener. Now observe that even though you've carefully made sure only the object being updated (using ie. Update) is being flushed to the database, NH still spends half it's time looking through all properties on all objects in the session (in the methods called by FlushEventListener.OnFlush).
ReplyDeleteI guess I'm in camp #2 and I really like this solution, however I have a problem where calling s.Load(id) does not work, while calling s.Get(id) does. Looks like when you call Load() the entity is set to read-only properly, but when OnSaveOrUpdate() is triggered the entity can not be found in the persistence context and thus will never be updated. Any ideas why this may be the case? Thanks for any help you can offer...
ReplyDeleteuNHAddins Rocks!
ReplyDeleteScott, I've had the exact same problem. I got round it because fortunately our architecure has some fundamental commonality (i.e. a base interface).
ReplyDeleteIn the ResetReadOnlyEntityListener I added the following to the ResetReadOnlyMethod:
EntityEntry entry = session.PersistenceContext.GetEntry(entity);
if (entry == null)
{
IDictionary currentKeys = session.PersistenceContext.EntitiesByKey;
EntityKey key = currentKeys.Keys.SingleOrDefault(e => ((Guid)e.Identifier) == ((IEntity)entity).Id);
entry = session.PersistenceContext.GetEntry(session.PersistenceContext.GetEntity(key));
}
This is before the existing check. It's not a solution everyone can use, but it works in out framework (or at least appears to so far!).
Not raised as a bug or AddIn's fix as it isn't a generic solution, but hopefully there is one out there...
hi fabio,
ReplyDeletefirst of all , i would to thank you for you help and for the wonderful unhaddins bibliothek that i am using for my wpf project.
i implemented the tip of the book "nhibernate 3 cookbook"
concerning "CbpT" pattern. In my modell i have an unidirectional many-to-one association in the child. Here is the example:
Title
-----
TitleId
Description
User
-----
UserId
TitleId
user has a many-to-one association with the table Title.
if i save the User say Session.save( User ) and ten call SaveAll() i get the exception "save transient entity Title" and dont get that because the cascade option is set to non.
you have to specify the cascade strategy in the mapping.
ReplyDeletethe nhusers google group is a good place for this kind of questions.
Hi Fabio,
ReplyDeleteThe cascading stategy is set to "none".
In the mapping file of user i have set this unidirectional many-to-one relationship as follow:
many-to-one
ReplyDeletename="Title"
column="TitleId"
class="Title"
cascade="none"
not-found="ignore"
How are things looking for NHibernate 3?
ReplyDeleteI am using the method RegisterDisableAutoDirtyCheckListeners and does not work , still update when I open a new transaction.
ReplyDeleteMy Code
config.AddAssembly(typeof(NHibernateHelper).Assembly);
config.SetProperty("connection.connection_string", ConfiguracaoHelper.StringConexao);
config.RegisterDisableAutoDirtyCheckListeners();
uNhaddins Version = 3.0.0.773
Nhibernate Version = 3.0.0.4000