28

I've just discovered that if I get an object from an NHibernate session and change a property on object, NHibernate will automatically update the object on commit without me calling Session.Update(myObj)!

I can see how this could be helpful, but as default behaviour it seems crazy!

Update: I now understand persistence ignorance, so this behaviour is now clearly the preferred option. I'll leave this now embarrassing question here to hopefully help other profane users.

How can I stop this happening? Is this default NHibernate behaviour or something coming from Fluent NHibernate's AutoPersistenceModel?

If there's no way to stop this, what do I do? Unless I'm missing the point this behaviour seems to create a right mess.

I'm using NHibernate 2.0.1.4 and a Fluent NHibernate build from 18/3/2009

Is this guy right with his answer?

I've also read that overriding an Event Listener could be a solution to this. However, IDirtyCheckEventListener.OnDirtyCheck isn't called in this situation. Does anyone know which listener I need to override?

rae1
  • 6,066
  • 4
  • 27
  • 48
Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
  • I don't think it's necessary to say that anyone asking this question is "profane". For examples, there are plenty of times when I want to load an object and I know I am not going to make changes to it. I don't want NHibernate to waste time checking if it changed. – scott.korin Aug 06 '18 at 03:13

4 Answers4

14

You can set Session.FlushMode to FlushMode.Never. This will make your operations explicit

ie: on tx.Commit() or session.Flush(). Of course this will still update the database upon commit/flush. If you do not want this behavior, then call session.Evict(yourObj) and it will then become transient and NHibernate will not issue any db commands for it.

Response to your edit: Yes, that guy gives you more options on how to control it.

Ben Scheirman
  • 40,531
  • 21
  • 102
  • 137
  • 1
    yeah but it will still happen when I do flush. – Andrew Bullock Mar 23 '09 at 14:55
  • ahh, then make the object transient by doing Session.Evict() – Ben Scheirman Mar 23 '09 at 14:57
  • I get the feeling nhibernate doesnt want me to control updates manually, all these solutions seem like hacks. why is this? – Andrew Bullock Mar 23 '09 at 15:17
  • 1
    because you can save an entire aggregate by calling save on the root entity. Saves can be cascaded to other objects. In addition, NH knows how to keep references from other objects in sync, so you don't save duplicates for example. – Ben Scheirman Mar 23 '09 at 15:19
  • 1
    oooook... I'm afraid im still not getting this 100%. I know what my cascade rules are, if im calling update then i should be aware of whats gonna happen, nh shouldn't try and save me from my own stupidity with strange features – Andrew Bullock Mar 23 '09 at 15:25
  • 1
    I think we need more details on what you're doing to give you a better answer. NH issues queries at the last possible moment, so it apparently *needs* it at the time when the query happens. otherwise it waits for tx.commit or session.flush. – Ben Scheirman Mar 24 '09 at 16:13
  • Changing FlushMode to Never will not disable dirty checking feature. It makes the only difference - you have to call Session.Flush explicitly. If you don't call it - no entities being saved, even entities you have passed to Session.Save. If you do call Session.Flush - ALL entities being saved, even ones you have not passed to Session.Save. – Serhiy Oct 10 '14 at 10:05
3

My solution:

  1. In your initial ISession creation, (somewhere inside your injection framework registrations) set DefaultReadOnly to true.
  2. In your IRepository implementation which wraps around NHibernate and manages the ISession and such, in the Insert, Update, InsertUpdate and Delete (or similar) methods which call ISession.Save, Update, SaveUpdate, etc., call SetReadOnly for the entity and flag set to false.
Mr. TA
  • 5,230
  • 1
  • 28
  • 35
1

Calling SaveOrUpdate() or Save() makes an object persistent. If you've retrieved it using an ISession or from a reference to a persistent object, then the object is persistent and flushing the session will save changes. You can prevent this behavior by calling Evict() on the object which makes it transient.

Edited to add: I generally consider an ISession to be a unit of work. This is easily implemented in a web app. using session-per-request but requires more control in WinForms.

Jamie Ide
  • 48,427
  • 16
  • 81
  • 117
  • After I Evict, can I manually do an update on the object? Is this whole approach a good idea? Is there some way to make this default behaviour for all objects or do i have to evict each one? – Andrew Bullock Mar 23 '09 at 15:09
  • After you Evict, the object is transient. To make it persistent again, you can call SaveOrUpdate on it. – Jamie Ide Mar 23 '09 at 15:17
  • I think you're trying to fight the session as a unit of work. There is a way to re-attach, but you should probably just rely on tighter session lifecycles. In a web app, this would be per request, in a smart client it would be per-unit-of-work. – Ben Scheirman Mar 23 '09 at 15:18
  • 8
    Ok, thanks for your help, I cant help feeling like this is a hacky solution though. Cant i just set NHibernate.DoThingsWithoutMeAsking = false; somewhere? – Andrew Bullock Mar 23 '09 at 15:19
  • Im in a webapp and have a UoW setup to commit on End_Request. My specific case is where i update an object from an edit screen, then validate it. If it fails validation i throw it back with errors, if it passes then i call save. Thing is its still saved on a failed validation because of this issue! – Andrew Bullock Mar 23 '09 at 15:26
  • In that case, I think the rule is: If the object doesn't fit, then you must Evict. If you weren't using session-per-request then you would control the UOW by calling Flush() on it. – Jamie Ide Mar 23 '09 at 15:34
  • Yes, you can either evict the object or change the EndREquest behavior to tx.Rollback or session.clear before disposing of it, so that you have the semantics of unless-I-commit-the-changes-they-don't-happen. – Ben Scheirman Mar 24 '09 at 16:20
  • @Ben, yes this seems like my only solution. It feels hacky though :s – Andrew Bullock Apr 02 '09 at 12:47
0

We did this by using the Event Listeners with NH (This isn't my work - but I can't find the link for where I did it...).

We have a EventListener for when reading in the data, to set it as ReadOnly - and then one for Save (and SaveOrUpdate) to set them as loaded, so that object will persist when we manually call Save() on it.

That - or you could use an IStatelessSession which has no State/ChangeTracking.

This sets the entity/item as ReadOnly immediately on loading.

I've only included one Insertion event listener, but my config code references all of them.

/// <summary>
/// A listener that once an object is loaded will change it's status to ReadOnly so that
/// it will not be automatically saved by NH
/// </summary>
/// <remarks>
/// For this object to then be saved, the SaveUpdateEventListener is to be used.
/// </remarks>
public class PostLoadEventListener : IPostLoadEventListener
{
    public void OnPostLoad(PostLoadEvent @event)
    {
        EntityEntry entry = @event.Session.PersistenceContext.GetEntry(@event.Entity);

        entry.BackSetStatus(Status.ReadOnly);
    }
}

On saving the object, we call this to set that object to Loaded (meaning it will now persist)

public class SaveUpdateEventListener : ISaveOrUpdateEventListener
{
    public static readonly CascadingAction ResetReadOnly = new ResetReadOnlyCascadeAction();

    /// <summary>
    /// Changes the status of any loaded item to ReadOnly.
    /// </summary>
    /// <remarks>
    /// Changes the status of all loaded entities, so that NH will no longer TrackChanges on them.
    /// </remarks>
    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.Entry);
        }
    }

    private static void CascadeOnUpdate(SaveOrUpdateEvent @event, IEntityPersister entityPersister, 
        object entityEntry)
    {
        IEventSource source = @event.Session;
        source.PersistenceContext.IncrementCascadeLevel();
        try
        {
            new Cascade(ResetReadOnly, CascadePoint.BeforeFlush, source).CascadeOn(entityPersister, entityEntry);
        }
        finally
        {
            source.PersistenceContext.DecrementCascadeLevel();
        }
    }
}

And we implement it into NH thus so:

    public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbConfig, Action<MappingConfiguration> mappingConfig, bool enabledChangeTracking,bool enabledAuditing, int queryTimeout)
    {
        return Fluently.Configure()
            .Database(dbConfig)
            .Mappings(mappingConfig)
            .Mappings(x => x.FluentMappings.AddFromAssemblyOf<__AuditEntity>())
            .ExposeConfiguration(x => Configure(x, enabledChangeTracking, enabledAuditing,queryTimeout))
            .BuildSessionFactory();
    }

    /// <summary>
    /// Configures the specified config.
    /// </summary>
    /// <param name="config">The config.</param>
    /// <param name="enableChangeTracking">if set to <c>true</c> [enable change tracking].</param>
    /// <param name="queryTimeOut">The query time out in minutes.</param>
    private static void Configure(NHibernate.Cfg.Configuration config, bool enableChangeTracking, bool enableAuditing, int queryTimeOut)
    {
        config.SetProperty(NHibernate.Cfg.Environment.Hbm2ddlKeyWords, "none");
        if (queryTimeOut > 0)
        {
            config.SetProperty("command_timeout", (TimeSpan.FromMinutes(queryTimeOut).TotalSeconds).ToString());
        }

        if (!enableChangeTracking)
        {
            config.AppendListeners(NHibernate.Event.ListenerType.PostLoad, new[] { new Enact.Core.DB.NHib.Listeners.PostLoadEventListener() });
            config.AppendListeners(NHibernate.Event.ListenerType.SaveUpdate, new[] { new Enact.Core.DB.NHib.Listeners.SaveUpdateEventListener() });
            config.AppendListeners(NHibernate.Event.ListenerType.PostUpdate, new[] { new Enact.Core.DB.NHib.Listeners.PostUpdateEventListener() });
            config.AppendListeners(NHibernate.Event.ListenerType.PostInsert, new[] { new Enact.Core.DB.NHib.Listeners.PostInsertEventListener() });
        }
    }
Stuart.Sklinar
  • 3,683
  • 4
  • 35
  • 89
  • Could you tell us why have you decided to do this ? Did you notice some gain in performance ? – yeska Jul 11 '16 at 17:51
  • We had some times were updating data model, but weren't sure if we wanted them saving (I think - this was 2 years ago). Possibly bad arch, as you should only really change what you want to save. Turning off tracking *should* theoretically improve performance - but you then have to save everything manually. – Stuart.Sklinar Jul 12 '16 at 09:42