NHibernate SaveOrUpdate Child Collection Not Updated With Identity Ids

13,518

Solution 1

I finally found the time to dig around in the NHibernate code... the reason this is happening makes sense... short answer - the Cascade actions are processed on Session.Commit(); well after the above service operation has completed. Thus, my child objects haven't been refreshed to reflect the assigned ids by the time the service method returns the updated child entity.

Turns out I must call SaveOrUpdate directly on child entity if I need to know the Id immediately.

Solution 2

For detached scenario, use Merge(formerly known as SaveOrUpdateCopy)

Example: http://www.ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html

Regarding:

Do I explicitly have to call SaveOrUpdate on the child (that sucks if that is the case)?

You don't need to, in fact that's where an ORM like NHibernate shines, it can save the whole object graph, and it really solves the impedance mismatch between the Object and database.

The physical implementation(for example, int or guid) of how a parent and child records(and sub-child records and so on) are linked is completely invisible from the app. You can concentrate well on your objects rather than the under-the-hood plumbing on what datatypes(int,guid,etc) your object relationships should use

[UPDATE]

You can still save the parent and the saving will cascade to children, and can get the child's id too. If you need to get the child id, get it after commit.

You cannot get the child id this way:

s.SaveOrUpdate(parentEntity);
Console.WriteLine("{0}", parentEntity.ChildIdentity.First().ChildId);
tx.Commit();

Must do this:

s.SaveOrUpdate(parentEntity);
tx.Commit();    
Console.WriteLine("{0}", parentEntity.ChildIdentity.First().ChildId);

Solution 3

You could call session.Flush(), which performs the inserts on the database. This is not ideal because of the impact to performance.

Solution 4

Maybe this will help (It's about Inverse attribute): http://www.emadashi.com/index.php/2008/08/nhibernate-inverse-attribute/

Share:
13,518

Related videos on Youtube

Chris Baxter
Author by

Chris Baxter

Updated on June 04, 2022

Comments

  • Chris Baxter
    Chris Baxter almost 2 years

    I am clearly missing something (hopefully obvious), and I have had no luck with Google so far.

    I have a Parent-Child relationship mapped as follows

    Simplified Parent Map

      public sealed class ParentMap : ClassMap<ParentEntity>
      {
        public ParentMap()
        {
          Table("Parent");
          Component(x => x.Thumbprint);
          Id(x => x.Id).GeneratedBy.Identity();
    
          Map(x => x.ServeralNotNullableProperties).Not.Nullable();
    
          HasMany(x => x.Children).KeyColumn("ChildId")
                                  .Inverse()
                                  .LazyLoad()
                                  .Cascade
                                  .AllDeleteOrphan();
    
          References(x => x.SomeUnrelatedLookupColumn).Column("LookupColumnId").Not.Nullable();
        }
      }
    

    Simplified Child Map

      public sealed class ChildMap : ClassMap<ChildEntity>
      {
        public ChildMap()
        {
          Table("Child");
          Component(x => x.Thumbprint);
          Id(x => x.Id).GeneratedBy.Identity();
    
          Map(x => x.MoreNotNullableProperties).Not.Nullable();
    
          References(x => x.Parent).Column("ParentId").Not.Nullable();
        }
      }
    

    Simplified Service Method Steps

    I then have a service method that retrieves Parent and adds a new Child via some domain method. The underlying NHibernate code boils down to:

    1) Session Opened on WCF AfterReceiveRequest (IDispatchMessageInspector)

    _sessionFactory.OpenSession();
    

    2) Retrieve existing instance of parent via .Query

    _session.Query<ParentEntity>()
            .Where(item => item.Id == parentId)
            .Fetch(item => item.SomeLookupColumn)
            .First();
    

    3) Add new 'Child' entity to 'Parent' via domain object method such as...

    public ChildEntity CreateChild()
    {
      var child = new ChildEntity{ Parent = this };
    
      Children.Add(child);
    
      return child;
    }
    

    4) Ultimately calls SaveOrUpdate on 'Parent' entity.

     // ChildEntity Id is 0
     _session.SaveOrUpdate(parentEntity)
     // Expect ChildEntity Id to NOT be 0
    

    Note: Use of SaveOrUpdate Does persist changes to database; use of Merge results in TransientObjectException mentioned below.

    5) Finally, transaction committed on WCF BeforeSendReply (IDispatchMessageInspector)

     _session.Commit();
    

    The Problem

    Typically when an entity is saved, after the call to SaveOrUpdate, the Id of said entity refects the Identity generated by the INSERT statement. However, when I add a new child entity to an existing parent, the associated entity in Children does not get assigned an Id. I need to pass this Id back to the caller so subsequent calls result in UPDATES and not additional INSERTS.

    Why is NHibernate not updating the underlying entity? Do I explicitly have to call SaveOrUpdate on the child (that sucks if that is the case)?

    Any thoughts on the matter greatly appreciated. Thanks!

    EDIT I should note, that the new child entity IS being saved as expected; I just don't get back the Id assigned.

    UPDATE Tried switching to .Merge(~) as suggested by Michael Buen below; resulting in a TransientObjectException telling me to explicitly save my modified child entity before calling merge. I also experimented with .Persist(~) to no avail. Any additional insight greatly appreciated.

    object is an unsaved transient instance - save the transient instance before merging: My.NameSpace.ChildEntity

  • Chris Baxter
    Chris Baxter about 13 years
    The database insert is actually happening (record added to DB); however the associated object in the session does not get updated with the generated Identity value.
  • Chris Baxter
    Chris Baxter about 13 years
    I will give Merge a try shortly and see if that makes a difference... hopefully it does the trick.
  • Chris Baxter
    Chris Baxter about 13 years
    Spoke a little too soon; on Commit(), I am getting a TransientObjectException 'object references an unsaved transient instance - save the transient instance before flushing.' ???
  • Alexandre Leites
    Alexandre Leites about 13 years
    Put Inverse(which I think should be the default) attribute on your HasMany mapping
  • Chris Baxter
    Chris Baxter about 13 years
    Unfortunately, Inverse is already defined in mapping (see ParentMap above).
  • Alexandre Leites
    Alexandre Leites about 13 years
    Do you happen to store the _session variable as static? Maybe NHibernate cache some object(hence the transient thing errors) if session is instantiated as static. Only the _sessionFactory should remain static
  • Chris Baxter
    Chris Baxter about 13 years
    The session is created during AfterReceiveRequest and is temporarily stored in a ThreadStatic variable for the life of the given service request. The session is disposed at the end of the service request during BeforeSendReply and is explicitly cleared out of the ThreadStatic variable. The service operation is starting/ending on same thread.
  • Chris Baxter
    Chris Baxter about 13 years
    Thanks for the input, but I figured it out. Cascade actions are processed on session commit it would seem, thus when I am returning the 'updated' child entity in my sevice method, those cascade actions hadn't actually been processed even though I thought SaveOrUpdate was processing those items.

Related