NHibernate - Cascade Merge to child entities fails for detached parent entity
Asked Answered
I

1

16

Current Approach

In an ASP.NET web forms app (using Spring.NET and NHibernate) we have an aggregate root (Person) whose details are captured across a number of screens/pages. The Person entity exists prior to entering into this workflow, and all changes made to the Person object graph are atomic, and so should only be flushed to database upon submission of the final screen.

To achieve this, we load the Person (lazily) from the database using NHibernate 3.2 the first time into the first page, and thereafter we load and save the serialized Person object graph to a HTTP Session variable as we page through the process.

After retrieving the Person out of the HTTP Session, it is in a detached state from the current NHibernate session, so we re-attach by invoking the Update() method on the current session, like so:

var sessionPerson = Session[PersonSessionName] as Person;
var currentSession = SessionFactory.GetCurrentSession();
currentSession.Update(sessionPerson);

Note: Using Lock() threw an exception, advising that the “reassociated object has dirty collection”.

When reattached, we can traverse through the object graph as expected, pulling data from the database for child entities which had not yet been loaded into memory.

Subset of Mapping Files

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="false" assembly="Domain" namespace=" TestApp.Domain">
  <class name="Person" table="Person">
    <id name="Id">
      <generator class="TestApp.CustomNHibernateHiLoGenerator, TestApp.Core" />
    </id>
    <property name="Name" not-null="false" />

    <bag name="PersonCountries" access="field.camelcase-underscore" cascade="all-delete-orphan">
      <key column="PersonId" foreign-key="FK_ PersonCountry_Person" not-null="true" />
      <one-to-many class="PersonCountry" />
    </bag>
  </class>

  <class name="Country" table="Country">
    <id name="Id">
      <generator class="TestApp.CustomNHibernateHiLoGenerator, TestApp.Core" />
    </id>
    ... No back reference to Person
  </class>
</hibernate-mapping>

Domain

public class PersonCountry : Entity, ICloneable
{
    // No properties of note
}

public class Person : Entity, ICloneable
{
    public virtual string Name { get; set; }
    public virtual IEnumerable<PersonCountry> PersonCountries { get; set; }
    ... 
    // More Properties
}

Flushing changes to database

.. // Code-behind
PricingService.Save(ProductContext.Pricing, forceMerge: true);            


public class PricingService : IPricingService
{
   [Transaction]  // Spring.NET transaction
   public Pricing Save(Pricing pricing, bool forceMerge = false)
   {            
      if(forceMerge)
      {
         CurrentSession.Merge(entity);
      }
      else
      {
         CurrentSession.SaveOrUpdate(entity);
      }
   }
}

When it comes time to flush all changes to the database, provided we only change Name, the change works as expected. However, adding a new Country item to Person causes the cascading of the Merge() on one-to-many relationships to fail with the following exception (oddly, removing a Country works fine).

NHibernate.StaleStateException: Batch update returned unexpected row count from update; actual row count: 0; Expected: 1

Any help would be greatly appreciated.

Impanel answered 7/12, 2012 at 16:27 Comment(2)
can you set unsaved value on id of country? It seems that thenew Country is not seen as new. Or Maybe there is a Country with id == 0 in dbTynan
So that we can avail of lazy loading, we need to do currentSession.Update(sessionPerson) to re-attach the detached Person object to the current NHibernate session. When that happens the newly added Country is assigned a new id, which it seems might be causing problems for NHibernate when it goes to merge. Is an entity that's in the first level cache, but not the database and it has an id other than 0 still viewed as transient?Impanel
T
6

every entity with a valid Id is viewed as persistent, thats why it tries to update it in merge but because it hasn't been saved yet it fails. Call session.Flush() after session.Update()

Tynan answered 13/12, 2012 at 20:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.