10

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

John G
  • 3,483
  • 1
  • 23
  • 12
Chris Baxter
  • 16,083
  • 9
  • 51
  • 72
  • Not sure if this helps but SaveOrUpdate has been deprecated in favor of Add (I liked the name SaveOrUpdate better, but it is what it is) – Milimetric Apr 12 '11 at 01:58

4 Answers4

13

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.

Chris Baxter
  • 16,083
  • 9
  • 51
  • 72
5

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);
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • I will give Merge a try shortly and see if that makes a difference... hopefully it does the trick. – Chris Baxter Apr 12 '11 at 12:25
  • Spoke a little too soon; on Commit(), I am getting a TransientObjectException 'object references an unsaved transient instance - save the transient instance before flushing.' ??? – Chris Baxter Apr 12 '11 at 13:22
  • Put Inverse(which I think should be the default) attribute on your HasMany mapping – Michael Buen Apr 12 '11 at 16:22
  • Unfortunately, Inverse is already defined in mapping (see ParentMap above). – Chris Baxter Apr 12 '11 at 16:32
  • 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 – Michael Buen Apr 12 '11 at 17:20
  • 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 Apr 12 '11 at 18:11
  • 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. – Chris Baxter Apr 12 '11 at 20:16
1

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

VikciaR
  • 3,324
  • 20
  • 32
1

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

Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
  • 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 Apr 12 '11 at 12:21