Chris Baxter
Chris Baxter

Reputation: 16353

NHibernate SaveOrUpdate Child Collection Not Updated With Identity Ids

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

Upvotes: 10

Views: 13917

Answers (4)

Michael Buen
Michael Buen

Reputation: 39393

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);

Upvotes: 5

Chris Baxter
Chris Baxter

Reputation: 16353

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.

Upvotes: 13

Stefan Steinegger
Stefan Steinegger

Reputation: 64628

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

Upvotes: 1

VikciaR
VikciaR

Reputation: 3412

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

Upvotes: 1

Related Questions