NHibernate: many-to-many relation fails to save child objects first (either: “cannot insert null” or: “transient object”)

StackOverflow https://stackoverflow.com/questions/1819851

Question

I have a consignment class that aggregates a FreightDateTime class. At the same time, the FreightDateTime class is also aggregated by the GoodsItem class. In the same manner, FreightDateTime is associated with a number of other classes that I left out for now.

To avoid a databasetable FreightDateTime with ConsignmentId foreign key, a GoodsItemId foreign key, etc. I decided that the association should be many-to-many. This way, NHibernate would generate an association table for each relationship instead (ConsigmentFreightDateTimes, GoodsItemFreightDateTimes), which makes more sense.

So, in the mapping file, the association looks e.g. like this:

<bag name="DateTimes" table="FreightDateTimes" lazy="false" cascade="all">
  <key column="ConsignmentId"/>
  <many-to-many class="Logistics.FreightDateTime, Logistics" column="DateTimeId" />
</bag>

Setting cascade to "all" yields:

System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'DateTimeId', table 'LogiGate.dbo.FreightDateTimes'; column does not allow nulls. INSERT fails. 

Setting cascade to "none" yields:

NHibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Logistics.FreightDateTime

In both cases, this means that NHibernate is trying to save the Consignment instance, although the child FreightDateTime instances have not been saved. In the first case, the foreign key is still 'null', which thus cannot be inserted in the resulting table, and in the second case, NHibernate is aware that the instance has not yet been saved, and thus throws the exception.

So the question is how I can get NHibernate to save all child instances first without explicitly telling it to do so. I have hunch that allowing nulls on column DateTimeId would do the trick, but I think that is neither desirable nor possible.

Was it helpful?

Solution

Try to map the association on the other side also but use the inverse="true" attribute on that side. So, create in the FreightDateTime mapping file a bag to map the association as many-to-many with the Consignment.

Also, I have answered a similar question here: What is the correct way to define many-to-many relationships in NHibernate to allow deletes but avoiding duplicate records

Reading the question and my answer maybe will help you understand what is going on with your many-to-many association and maybe give you a hint for the solution. The final recommendation is kind of last resort.

The answer in the above question is just to give an idea of what is going through another person's different issue.

A solution would be to explicitly map the association table. If your tables are: Person, Note and the association table (X table) is PersonNote Your mappings should look like that:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="..."
                   namespace="...">

  <class name="Person" table="Person" lazy="true">

    <id name="PersonId">
      <generator class="native" />
    </id>
    <property name="FirstName" />
    .....
    <bag name="PersonNotes" generic="true" inverse="true" lazy="true" cascade="none">
        <key column="PersonId"/>
        <one-to-many class="PersonNote"/>
    </bag>

    <bag name="Notes" table="PersonNote" cascade="save-update">
      <key column="PersonId"></key>
      <many-to-many class="Note" column="NoteId"></many-to-many>
    </bag>

  </class>

</hibernate-mapping>

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="..."
                   namespace="...">

  <class name="Note" table="Note" lazy="true">

    <id name="NoteId" unsaved-value="0">
      <generator class="native" />
    </id>
    <property name="Title" />
    ....
    <bag name="PersonNotes" inverse="true" lazy="true" cascade="all-delete-orphan">
        <key column="NoteId"/>
        <one-to-many class="PersonNote"/>
    </bag>

    <bag name="People" table="PersonNote" inverse="true" cascade="save-update" generic="true">
      <key column="NoteId"></key>
      <many-to-many class="Person" column="PersonId"></many-to-many>
    </bag>

  </class>

</hibernate-mapping>

As it is above it allows you the following:

  1. Delete a Person and only delete the entry in the association table without deleting any of the Notes
  2. Delete an Note and only delete the entry in the association table without deleting any of the Person entities
  3. Save with Cascades from only the Person side by populating the Person.Notes collection and saving the Person.
  4. Since the inverse=true is necessary in the Note.People there isn't a way to do cascading save from this side. By populating the Note.People collection and then saving the Note object you will get an insert to the Note table and an insert to the Person table but no insert to the association table. I guess this is how NHibernate works and I haven't yet found a way around it.
  5. You can cascade save entries in the association table only explicitly by adding new items in the PersonNotes collection of the Note entity.

All the above are tested with unit tests. You will need to create the PersonNote class mapping file and class for the above to work.

If you need another class to have notes lets say Organisation then you only ad another association table to your schema called OrgnanisationNote and you do the same as above to the Organisation mapping file and the Note mapping file.

I must say again that this should be the final option for anyone who want to have complete control over his/hers many-to-many associations.

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