Question

When using a mapping such as:

<class name="Product">
  <id name="Id">
    <generator class="guid" />
  </id>
  <property name="Name"/>
  <map name="UserAddedFields" table="UserAddedFields" >
    <key column="parent_id"/>
    <index-many-to-many column="UserAddedFieldId" class="UserAddedField"/>
    <element column="fieldValue"/>
  </map>
</class>

And I wanted to add a new UserAddedField to an existing Product I need to first save the UserAddedField or I will get a TransientObjectException. The exception seems to imply I could set a cascade action that would make it autosave but nothing I've tried seems to work. Is this not possible?

///////////////////////////////////////////////

After the suggestion below I've changed the mapping to the following but I'm getting a StaleStateException: Batch update returned unexpected row count from update; actual row count: 0; expected: 1

      <class name="Product">
    <id name="Id">
      <generator class="guid" />
    </id>
    <property name="Name"/>
    <bag name="UserAddedFields" lazy="true" inverse="true" batch-size="25" cascade="all-delete-orphan">
      <key column="parentId" />
      <one-to-many class="UserAddedFieldSetting" />
    </bag>
  </class>
  <class name="UserAddedField" >
    <id name="Id">
      <generator class="guid" />
    </id>
    <property name="Name" />
    <property name="IsGlobal" type="bool"/>
  </class>
  <class name="UserAddedFieldSetting" >
    <id name="Id">
      <generator class="guid" />
    </id>
    <property name="FieldValue" />
    <many-to-one class="Product" name="Product" />
    <many-to-one class="UserAddedField" name="UserAddedField" cascade="all"/>
  </class>
Was it helpful?

Solution

As you've already experienced, cascading is not supported on the index element/mapping. While elements like

do support cascade, the index-many-to-many is just another index, which nature is not intended to be cascade supporting.

My suggestion would be: do not use the map. Introduce brand new object instead, represented by table UserAddedFields. If we'd extend it with a surrogated primary key, we can have an object like:

public class UserAddedFieldSetting
{
   public virtual int Id { get; protected set; }

   public virtual Product Product { get; set; }
   public virtual UserAddedField UserAddedField { get; set; }
   public virtual decimal FieldValue { get; set; }

which can be mapped as a <bag>, i.e: IList<UserAddedFieldSetting>. Not only it will support cascading on the many-to-one mapping of the UserAddedField, but also the querying will be much more simple and straightforward

<bag name="UserAddedFields" lazy="true" inverse="true" 
     batch-size="25" 
     cascade="all-delete-orphan">
  <key column="parent_id" />
  <one-to-many class="UserAddedFieldSetting" />
</bag>

EXTEND:

So, now, we have a mapping (see updated part in the question), which is working as needed - well almost. The issue is, that NHibernate is executing the SaveOrUpdate() on our many-to-one relation.

The problem here is, how to decide, that SaveOrUpdate should INSERT or UPDATE? And NHibernate decided to UPDATE by the way... but the row count returned was 0. Because there was in fact no row to update. We need to insert.

The key to the answer is in the mapping of the "unsaved value", the value representing the new == "to be inserted" state.

So let's extend the mapping:

<class name="UserAddedField" >
  <id name="Id" unsaved-value="00000000-0000-0000-0000-000000000000">
    <generator class="guid" />
  </id>
  ...

And be sure, that every new C# instance of the UserAddedField has the

Id = Guid.Empty;

Now, NHibernate, when the SaveOrUpdate is called, will know, that Guid.Empty means do INSERT...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top