Try fast search NHibernate

31 March 2009

Ensuring updates on Flush

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

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.



kick it on DotNetKicks.com

38 comments:

  1. Hi Fabio,

    Nice 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

    ReplyDelete
  2. You are one of the first assertion.

    As 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".

    ReplyDelete
  3. Yes, I'm certainly with the first assertion.

    Thanks Fabio

    ReplyDelete
  4. 1 - it would be interesting to have this feature ready-to-use in the future, switchable via configuration file

    2 - I prefer the default behaviour in order to discover unexpected modification

    ReplyDelete
  5. @tomb
    1- Only available out-side NH, probably in uNhAddIns.

    2- That was the matter: somebody don't want to persist "unexpected modification".

    ReplyDelete
  6. Fabio,

    I 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

    ReplyDelete
  7. 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.

    ReplyDelete
  8. Hi Fabio,

    I 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

    ReplyDelete
  9. Hi Fabio,

    The 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.

    ReplyDelete
  10. I'll check. My examples, may be, is not in sync with the last NH.

    ReplyDelete
  11. Seems a nice solution , since I don't like the auto-flush on dirty objects. :)
    However, 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

    ReplyDelete
  12. In the source, I see that the constructor on the abstract CascadeAction class is 'internal'. Perhaps it should be set to 'protected' ?

    ReplyDelete
  13. Ahhhh this solution, as many others in this site, is for NH2.1.x.

    ReplyDelete
  14. Since 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)

    ReplyDelete
  15. Now all is in uNhAddIns with an extension to register events:
    configuration.RegisterDisableAutoDirtyCheckListeners();

    ReplyDelete
  16. Nice, but it seems that this doesn't work when you call 'SaveOrUpdate' on the modified entity, instead of 'Update'.
    When 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 ?

    ReplyDelete
  17. ;) good issue. I'll write a test then create the issue in the uNhAddIns issue tracker and then fix it.
    Thanks Frederik.

    ReplyDelete
  18. The SaveOrUpdateCopy (that is a Merge) shouldn't be a problem... btw a test will confirm it.

    ReplyDelete
  19. All should work fine with the implementation on uNhAddIns.
    Thanks Frederik.

    ReplyDelete
  20. With pleasure;
    question: why should those listeners be serializable ?

    ReplyDelete
  21. The configuration is serializable.

    ReplyDelete
  22. ok.

    I'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.

    ReplyDelete
  23. This comment has been removed by the author.

    ReplyDelete
  24. Hi Carlos,

    Just 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.

    ReplyDelete
  25. 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.
    Is there a feasible way around this ?

    ReplyDelete
  26. @Lone
    This article is part of the solution because NH does not need to check the state of any obj.

    ReplyDelete
  27. 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.

    ReplyDelete
  28. With this impl NH does not spent time checking which are dirty entities.

    ReplyDelete
  29. I 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).

    ReplyDelete
  30. I 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...

    ReplyDelete
  31. Scott, I've had the exact same problem. I got round it because fortunately our architecure has some fundamental commonality (i.e. a base interface).

    In 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...

    ReplyDelete
  32. hi fabio,
    first 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.

    ReplyDelete
  33. you have to specify the cascade strategy in the mapping.
    the nhusers google group is a good place for this kind of questions.

    ReplyDelete
  34. Hi Fabio,
    The cascading stategy is set to "none".
    In the mapping file of user i have set this unidirectional many-to-one relationship as follow:

    ReplyDelete
  35. many-to-one
    name="Title"
    column="TitleId"
    class="Title"
    cascade="none"
    not-found="ignore"

    ReplyDelete
  36. How are things looking for NHibernate 3?

    ReplyDelete
  37. I am using the method RegisterDisableAutoDirtyCheckListeners and does not work , still update when I open a new transaction.

    My 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

    ReplyDelete