2

I have two Nhibernate mappings for two classes, Category and Product. My Category class has two properties that are collections. The Children property is a collection of type Category which represents child categories (represents a category menu, typical parent child scenario). The second property on the Category class is a Products collection which represents all the products under a category.

What I am trying achieve is when I delete a category I want the category to deleted but not the product. So I want the product to be orphaned. i.e have its foreign key (CategoryId) in the Product table set to null. I don't want to delete a product just because I have deleted a category. I want to be able to reassign in at a later time to another category. My mappings representing the mentioned scenario are below.

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="naakud.domain" namespace="naakud.domain">
  <class name="Category">
    <id name="Id">
      <generator class="hilo" />
    </id>
    <version name="Version"/>
    <property name="Name" not-null="true" unique="true" />
    <set name="Products"
         cascade="save-update"
         inverse="true"
         access="field.camelcase-underscore">
      <key column="CategoryId" foreign-key="fk_Category_Product" />
      <one-to-many class="Product" />
    </set>
    <many-to-one name="Parent" class="Category" column="ParentId" />
    <set name="Children"
         collection-type="naakud.domain.Mappings.Collections.TreeCategoriesCollectionType, naakud.domain"
         cascade="all-delete-orphan"
         inverse="true"
         access="field.camelcase-underscore">
      <key column="ParentId" foreign-key="fk_Category_ParentCategory" />
      <one-to-many class="Category"/>
    </set>
  </class>
</hibernate-mapping>


<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="naakud.domain" namespace="naakud.domain">
  <class name="Product">
    <id name="Id">
      <generator class="hilo" />
    </id>
    <version name="Version" />
    <property name="Name" not-null="true" unique="true" />
    <property name="Description" not-null="true" />
    <property name="UnitPrice" not-null="true" type="Currency" />
    <many-to-one name="Category" column="CategoryId" />
  </class>
</hibernate-mapping>

With this mapping, when I delete a category which has products associated with it I get the following constraint error.

The DELETE statement conflicted with the REFERENCE constraint "fk_Category_Product". The conflict occurred in database "naakud", table "dbo.Product", column 'CategoryId'. The statement has been terminated.

However, when I remove the inverse=true attribute on the Products collection in the Category mapping then it works fine. My CategoryId foreign key in the products table is set to null and thus disassociating a product with a category. Which is what I want.

I have read about the inverse attribute and I understand it signifies the owning side of a relationship and updates/inserts/deletes are done in a different order which is why I think it solves my problem. So my question is, am I solving my problem in the correct way? How does this affect performance? (not much I suspect). Would it be better to have a uni-directional relationship without the many to one side and have the inverse attribute set to true to get better performance? Or am I going crazy and completely missing the point?

dezzy
  • 479
  • 6
  • 18

2 Answers2

1

Another way of fixing the delete problem is by setting the many-to-one property to null on all the related entities to null before flushing.

I can think of at least two ways to do it:

  • In the same method that calls session.Delete(category), do:

    foreach (var product in category.Products)
        product.Category = null;
    
  • Using HQL:

    session.CreateQuery(
           "update Product set Category = null where Category = :category")
           .SetParameter("category", category)
           .ExecuteUpdate();
    

Update:

Here's a proof-of-concept implementation using an event listener.

Diego Mijelshon
  • 52,548
  • 16
  • 116
  • 154
  • Hi Diego. Thanks for the answer. I did think of this but at the moment I am writing category tests to check my cascades are working correctly and hence if I can get the mentioned behavior using Nhibernate cascade functionality then I would prefer that. – dezzy Mar 20 '11 at 08:51
  • @dezzy: NHibernate does not support an "on delete set null" cascading behavior. You might be able to fake it by using event listeners, but it's more work. – Diego Mijelshon Mar 20 '11 at 11:05
  • You got me thinking about this... if I can get it working, I'll write a blog post about it (http://sessionfactory.blogspot.com, stay tuned :-)) – Diego Mijelshon Mar 20 '11 at 11:32
  • Well, I think I am faking it now by taking off the inverse=true attribute on the Products collection. The trade off is the extra update statement, which I can live with. I was just wondering whether this was the right way to go about things. Seems a reasonable solution to functionality that is not built in. So I`m happy. Thanks again. Much appreciate the input. – dezzy Mar 20 '11 at 11:33
0

I assume that you read about Inverse Attribute in NHibernate

As the error message says, your DELETE generates a conflict with the foreign key constraint, meaning that the DB cannot delete the Category as long as there are Products referencing that particular Category.

What you could do (if you can alter the DB schema) is applying "ON DELETE SET NULL" to your foreign key constraint. That way, when the DELETE is executed, the DB will automatically set all references in the Product table to NULL.

If you cannot modify the foreign key, then you would have little choice but to remove the inverse attribute. Doing so will result in NHibernate first setting the Product.Category reference to NULL and then deleting the Category.

If you need Product.Category fairly often then you should not get rid of the many-to-one attribute in Product.

Regarding the performance, that depends on how often you insert Products. Each insert will result in an additional update to set the foreign key. That should not be a problem, though.

Community
  • 1
  • 1
Florian Lim
  • 5,332
  • 2
  • 27
  • 28
  • Yes I have read about inverse and I know its purpose. I am free to modify the schema but I think I will set the inverse attribute to false as I would like configuration in one place. I also like the idea of having the database maintain referential integrity for me. I am using NHibernate in the context of an administration application so updates will be minimal and user driven. I wrote this post quite late and after thinking more the question I want to ask is why does Nhibernate not detect this relationship and set the inverse appropriatley? After all all other values have sensible defaults. – dezzy Mar 18 '11 at 06:41
  • Thanks for the response by the way. Much appreciated. Couldn't fit it into my first comment ;) – dezzy Mar 18 '11 at 06:43