views:

1008

answers:

3

Hi,

I've been fighting with an NHibernate set-up for a few days now and just can't figure out the correct way to set out my mapping so it works like I'd expect it to.

There's a bit of code to go through before I get to the problems, so apologies in advance for the extra reading.

The setup is pretty simple at the moment, with just these tables:

Category
CategoryId
Name

Item
ItemId
Name

ItemCategory
ItemId
CategoryId

An item can be in many categories and each category can have many items (simple many-to-many relationship).

I have my mapping set out as:

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

  <class name="Category" lazy="true">

    <id name="CategoryId" unsaved-value="0">
      <generator class="native" />
    </id>
    <property name="Name" />

    <bag name="Items" table="ItemCategory" cascade="save-update" inverse="true" generic="true">
      <key column="CategoryId"></key>
      <many-to-many class="Item" column="ItemId"></many-to-many>
    </bag>

  </class>

</hibernate-mapping>

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

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

    <id name="ItemId" unsaved-value="0">
      <generator class="native" />
    </id>
    <property name="Name" />

    <bag name="Categories" table="ItemCategory" cascade="save-update" generic="true">
      <key column="ItemId"></key>
      <many-to-many class="Category" column="CategoryId"></many-to-many>
    </bag>

  </class>

</hibernate-mapping>

My methods for adding items to the Item list in Category and Category list in Item set both sides of the relationship.

In Item:

    public virtual IList<Category> Categories { get; protected set; }
    public virtual void AddToCategory(Category category)
    {
        if (Categories == null)
            Categories = new List<Category>();

        if (!Categories.Contains(category))
        {
            Categories.Add(category);
            category.AddItem(this);
        }
    }

In Category:

    public virtual IList<Item> Items { get; protected set; }
    public virtual void AddItem(Item item)
    {
        if (Items == null)
            Items = new List<Item>();

        if (!Items.Contains(item))
        {
            Items.Add(item);
            item.AddToCategory(this);
        }
    }

Now that's out of the way, the issues I'm having are:

  1. If I remove the 'inverse="true"' from the Category.Items mapping, I get duplicate entries in the lookup ItemCategory table.

  2. When using 'inverse="true"', I get an error when I try to delete a category as NHibernate doesn't delete the matching record from the lookup table, so fails due to the foreign key constraint.

  3. If I set cascade="all" on the bags, I can delete without error but deleting a Category also deletes all Items in that category.

Is there some fundamental problem with the way I have my mapping set up to allow the many-to-many mapping to work as you would expect?

By 'the way you would expect', I mean that deletes won't delete anything more than the item being deleted and the corresponding lookup values (leaving the item on the other end of the relationship unaffected) and updates to either collection will update the lookup table with correct and non-duplicate values.

Any suggestions would be highly appreciated.

Thanks,

Kev

+1  A: 

Are you keeping the collections in synch? Hibernate expects you, I believe, to have a correct object graph; if you delete an entry from Item.Categories, I think you have to delete the same entry from Category.Items so that the two collections are in sync.

RMorrisey
I've got remove method similar to the add methods in the samples above and these work okay, it's just the deletion of an actual object that I'm having trouble with.I suppose I could loop through the collection of related items in an object when deleting it but I thought NHibernate should be picking this up.
Kevin Wilson
Typically I'd map this sort of association with an actual ItemCategory object that can be deleted. I'm not as sure with the direct association like you have, how to fix it. Sorry I couldn't be more help. =/
RMorrisey
So, if I created an ItemCategory object and mapping, that would mean that both Item and Category would have a one-to-many association with ItemCategory and also have a collection of ItemCategory objects? Would I then do something like Item.ItemCategories.Categories to get, say, the Categories for an Item? Or, is it possible to use an ItemCategory mapping object but keep the domain model 'clean' with just Item.Categories and Category.Items?
Kevin Wilson
An ItemCategory object would map to one Item and one Category, just like you have it for the table schema. Each Category would have a collection of ItemCategories, and each Item would have a collection of ItemCategories; so it would be Item.ItemCategories[i].Category. You could conceivably implement a helper method Item.Categories for ease of use. It's possible to do it directly without having an ItemCategory object, the way you put it originally, but it's more difficult IMHO and I don't know the details as well.
RMorrisey
+4  A: 

What you need to do in order to have your mappings work as you would expect them to, is to move the inverse="true" from the Category.Items collection to the Item.Categories collection. By doing that you will make NHibernate understand which one is the owning side of the association and that would be the "Category" side.

If you do that, by deleting a Category object it would delete the matching record from the lookup table as you want it to as it is allowed to do so because it is the owning side of the association.

In order to NOT delete the Items that are assigned to a Category object that is to be deleted you need to leave have the cascade attribe as: cascade="save-update".

cascade="all" will delete the items that are associated with the deleted Category object.

A side effect though would be that deleting the entity on the side where the inverse=tru exists will thow a foreign key violation exception as the entry in the association table is not cleared.

A solution that will have your mappings work exactly as you want them to work (by the description you provided in your question) would be to explicitly map the association table. Your mappings should look like that:

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

  <class name="Category" lazy="true">

    <id name="CategoryId" unsaved-value="0">
      <generator class="native" />
    </id>
    <property name="Name" />

    <bag name="ItemCategories" generic="true" inverse="true" lazy="true" cascade="none">
        <key column="CategoryId"/>
        <one-to-many class="ItemCategory"/>
    </bag>

    <bag name="Items" table="ItemCategory" cascade="save-update" generic="true">
      <key column="CategoryId"></key>
      <many-to-many class="Item" column="ItemId"></many-to-many>
    </bag>

  </class>

</hibernate-mapping>

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

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

    <id name="ItemId" unsaved-value="0">
      <generator class="native" />
    </id>
    <property name="Name" />

    <bag name="ItemCategories" generic="true" inverse="true" lazy="true" cascade="all-delete-orphan">
        <key column="ItemId"/>
    <one-to-many class="ItemCategory"/>
    </bag>

    <bag name="Categories" table="ItemCategory" inverse="true" cascade="save-update" generic="true">
      <key column="ItemId"></key>
      <many-to-many class="Category" column="CategoryId"></many-to-many>
    </bag>

  </class>

</hibernate-mapping>

As it is above it allows you the following:

  1. Delete a Category and only delete the entry in the association table without deleting any of the Items
  2. Delete an Item and only delete the entry in the association table without deleting any of the Categories
  3. Save with Cascades from only the Category side by populating the Category.Items collection and saving the Category.
  4. Since the inverse=true is necessary in the Item.Categories there isn't a way to do cascading save from this side. By populating the Item.Categories collection and then saving the Item objec you will get an insert to the Item table and an insert to the Category 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.

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

tolism7
If I reverse the "inverse='true'" as suggested, I can delete categories as expected but then I have the reverse of the problem I had previously: I get an error when I try to delete an Item as it doesn't delete the lookup value.
Kevin Wilson
I have done some more investigation on the subject and edited my answer to include my findings.
tolism7
Thanks for the update - that was a massive help. Sorry about the late reply, I didn't get an email through letting me know that you'd updated your answer.
Kevin Wilson
As I can see I do not have to map the relationship table in a seperate hbm xml file, have I?
Rookian
@Rookian In the example above the ItemCategory database table is mapped in its own XML file and it has 2 many-to-one properties one that points to the Item table and one that points to the Category table which are the opposite side of the one-to-many properties in the Item class mapping file (ItemCategories property) and the Category class mapping file (ItemCategories property).
tolism7
I do not understand why you use one-to-many associations AND many-to-many oO? Why not only one-to-many's? An explanation in more detail would be nice :)
Rookian
Including both gives you great flexibility on accessing and manipulating data though cascades. You have a direct link to the association table and a direct link to the other side so you can access data and manage the relationship as you wish. It also comes down to performance; If you want to receive information for example on whether or not a Category has at least one Item then if you use the many -to-many relationship your query will be joining 3 tables (Category-ItemCategory-Item) but if you use the one-to-many relationship you will be joining only 2 tables (Category-ItemCategory).
tolism7
...Also when it comes to save-update-delete operations if you include the one-to-many relationship on the Item class with the cascade as in the example, only then you will be able to Delete an Item and only delete the entry in the association table without deleting any of the Categories or without getting an exception.
tolism7
I can't figure it out not really. I ask myself how does the object model looks like and how are the DIFFERENT associations implemented as well as the mapping using FNH? Particularly how do you synchronize the lists of Categories/Items and the ItemCategories?
Rookian
I don't as I do not need to. NHibernate does most of the work for me as all of the collections are lazy. If you add an Category to an Item through the many-to-many association then the ItemCategories collection of both objects will not be refreshed until the Category is persisted. That is acceptable for me. Personally I do not need my collections (i.e. in the Item class the ItemCategories and the Categories properties) to be synchronized at the minute I add an element to one of them as it does not affect the my domain's rules.
tolism7
... If you need to have users of your code not being able to access one or the other to avoid synchronization issues, just map the one-to-many to a private member of your entity with access="field". The above solution offers the most control over your objects' relationship. I cannot say I recommend this to anyones object model but it works great if you can manage it.
tolism7
Does the cascading in your solution work for both sides (Category and Item)? So it does not matter if I save the Category or the Item, when I create a Category and an Item. I would highly appreciate if you would be so nice and publish your complete solution in a blog or so :).
Rookian
Actually no. Please read point 4 at the answer above. Thanks for the encouragement about writing a blog post. I do not think it is necessary though as the answer above states all important bits of the idea behind it. As soon as I have some time and a blog I will will try to write that in a post. Thanks.
tolism7
ok =) well ... I ask myself "It is possible to make BOTH SIDES responible for the cascading operations by using 2 many to one assocations where on the left-middle (Item-ItemCategory) assocation the left side(Item) is the parent and on the middle-right (ItemCategory-Category) assocation the right side(Category) is the parent"?
Rookian
I believe it is possible. At it is above in the answer I provided it wouldn't work. To make it work you will need to either remove completely the many-to-many associations from the Item and Category mapping files and leave ONLY the one-to-many with the ItemCategory OR just remove the cascades from the many-to-many associations and have both one-to-many associations with cascade="all-delete-orphan".
tolism7
I only used one-to-many assocations but it does not work ... look there http://stackoverflow.com/questions/1928591/nhibernate-many-to-many-assocations-using-a-relation-entity-in-the-domain-model
Rookian
A: 

Hi,

I have tried out the modified mappings and it works for deletion of category and corresponding all records from itemcategory is also removed. But the issue that I am getting now is I am not able to save items corresponding to a category in database. It does nothing on save or update. What could be wrong ?

sachin