views:

964

answers:

1

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.

+1  A: 

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: http://stackoverflow.com/questions/1695063/what-is-the-correct-way-to-define-many-to-many-relationships-in-nhibernate-to-all/

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.

tolism7
Adding an inverse bag to the FreightDateTime mapping will have NHibernate require that there is also a corresponding IList property in the FreighDateTime class. My goal is to avoid referencing FreightDateTime to Consignment both at class and table level, because FreightDateTime should not need to "know" anything about the objects owning it. How could that be implemented?
Fedor Steeman
I believe there is no way around it. You must have the inverse bag created in order for your mapping to work as you want it to. What NHibernate allows you is to map the bag to a private member instead of a public property (access="field" attribute) that wouldn't be accessible from outside the entity's implementation.If you still insist on not mapping the inverse then just experiment to see if it does work by having it there. Then you can have it working until you find the "perfect" solution for you (if there is one).
tolism7
Thanks. I think I would rather have a bunch of one-to-many relations, that will probably result in the FreightDateTimes-table refering to a lot a different other tables that are unrelated to each other, but so be it for the time being. I also looked at composite-element and many-to-any, but these are meant for different situations. Or what do you think?
Fedor Steeman
I could also try a different approach and abolish the FreightDateTime class altogether and use composite-element instead. The backside of that would be that my database would be cluttered up with small tables, but it would be that anyways with the association tables I envisaged...
Fedor Steeman
Explicitly mapping the association table (FreightDateTimes) and have the many-to-many relationship converted to 2 one-to-many (one per table) with the association table is always a good alternative in my opinion. As I presented in the answer I gave to the other relevant question it can give you mach benefits if used right.
tolism7
Hmmm... explicitly mapping the association table is something I did not consider yet and may be a good alternative as you say. How will this exactly look like in the mapping files that, after all, should refer to one specific class? I am rather fuzzy on that... Could you put an example in a new answer?
Fedor Steeman
I have edited my answer to include an example as you've asked. I did not use the entities you have as I could not figure out exactly which ones were involved. I believe the example is simple and you will be able to port it easily to your own schema if needed.
tolism7