NHibernate SaveOrUpdate Child Collection Not Updated With Identity Ids
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/
Related videos on Youtube
Chris Baxter
Updated on June 04, 2022Comments
-
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 about 13 yearsThe 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 about 13 yearsI will give Merge a try shortly and see if that makes a difference... hopefully it does the trick.
-
Chris Baxter about 13 yearsSpoke 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 about 13 yearsPut Inverse(which I think should be the default) attribute on your HasMany mapping
-
Chris Baxter about 13 yearsUnfortunately, Inverse is already defined in mapping (see ParentMap above).
-
Alexandre Leites about 13 yearsDo 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 about 13 yearsThe 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 about 13 yearsThanks 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.