nhibernatemappingguidforeign-key-relationshipmany-to-one

NHibernate many-to-one mapping: If parent is null, set foreign-key as empty Guid instead of null


What I am trying to is really quite straigh forward, but I cannot seem to get the mapping right with NHibernate.

I am working up against a database with parent and child objects. The child objects have a foreign key reference to the primary key of the parent which is of datatype Guid. Pretty normal in any case. Now the database is set up in such a way that the foreign key field must never be null, so in case of orphan objects, with no parent, the foreign key should be an empty Guid ('00000000-0000-0000-0000-000000000000').

The way I set up Nhibernate is has been working fine for a long time, but recently I made the relationships bidirectional and then problems started arising. Obviously, NHibernate will see that the parent is null and attempt to save null to the foreign key field, but that is not allowed!

Example of the structure of the relationship mapping I am using follows below.

Parent side mapping:

<id name="ID" column="ID">
    <generator class="guid" />
</id>
<bag name="Children" table="Children" lazy="false" cascade="all" inverse="true">
  <key column="FK_OwnerID" not-null="true"/>
  <one-to-many class="Childclass"/>
</bag>

Child side mapping:

<many-to-one name="Owner" column="FK_OwnerID" not-found="ignore" not-null="false" class="OwnerClass"/>

I have been trying with different properties but of no avail. Am I forced to use the insert="false" and update="false" properties and if so, how do I maintain the relationships exactly?

Thanks in advance for any help.


Solution

  • If I understand it correctly your DB structure is not enforcing referential integrity (FK_OwnerID is not really a foreign key) but through your changes in the mapping you are telling NHibernate to enforce just that. I think that cannot work.

    Basically, I see two options do solve this issue:

    1. Create a dummy entry with your 000-Guid in your DB that acts as a parent for all those orphans.
    2. Alter your table to allow NULL in that column and make it a real foreign key constraint (with "on cascade set null). (I would recommend that.)

    Edit: If option 1 is not an option then I suggest option 3: You could try NHibernate's Interceptor feature. You need to implement IInterceptor (or inherit from EmptyInterceptor). The OnSave() method is used for new objects, the OnFlushDirty is used for changed objects. What we do here is creating a new "virtual" object with the desired ID and assign it.

    using System;
    using NHibernate;
    
    namespace NameSpaceWithDAL
    {
        public class TestInterceptor : NHibernate.EmptyInterceptor
        {
            public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, NHibernate.Type.IType[] types)
            {
                if (entity is ChildClass && (entity as ChildClass).Owner == null)
                {
                    SetState(propertyNames, state, "Owner", new OwnerClass { ID = "000..." });
                }
    
                return true;
            }    
    
            public override bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, NHibernate.Type.IType[] types)
            {
                if (entity is ChildClass && (entity as ChildClass).Owner == null)
                {
                    SetState(propertyNames, state, "Owner", new OwnerClass { ID = "000..." });
                }
    
                return true;
            }
    
            private void SetState(string[] propertyNames, object[] state, string propertyname, object value)
            {
                var index = Array.IndexOf(propertyNames, propertyname);
                if (index == -1) return;
                state[index] = value;
            }    
        }
    }
    

    In order to use that, the interceptor needs to be defined either for the session (in OpenSession()) or in the overall configuration:

    new Configuration().SetInterceptor(new TestInterceptor());
    

    I tested the above code with simple properties, so I cannot say if that actually works with relationships.

    Code examples taken from Mike O'Brien