views:

448

answers:

5

I have a SQL Server DB with a recursive table:

MyTable:
 ID : string PrimaryKey
 Parent: string references MyTable - NOTNULL !!

and map with Fluent NHibernate to

class MyTable
{
  public virtual string ID {get; set;}
  public virtual MyTable Parent {get; set;}
}

My problem is that Parent should be null in my C# app if the column Parent is "" (empty string) in the database and vice versa. Unfortunately I can't change the column type to accept NULL!

I tried to use IEmptyInterceptor but I don't get it working.

Thanks in advance, forki

+1  A: 

I'd go for a IUserType which would convert empty string to nulls and vice versa. Two methods to pay attention to are NullSafeGet and NullSafeSet.

Not sure though how custom types integrate with Fluent NHibernate.

Anton Gogolev
I tried this. But unfortunately I didn't get this working. Do I have to implement IUserType for the class MyTable?
forki23
Your domain class should not implement the IUserType interface, instead you would create a separate user type class in the data access layer, which then will be used in the mapping.That way your domain layer still won't have to now about the data access layer. To specify user types in the Fluent NHibernate mapping, use the .CustomTypeIs<MyUserType>() method.
Erik Öjebo
IUserType is definitely the way to go. Create an implementation of that interface separate to everything else, which will be used to transform null strings to empty ones and vice-versa. You then specify this implementation in your fluent mappings using CustomTypeIs as Erik showed.
James Gregory
Hi Erik and James,could you please tell me what I am doing wrong? Please see code in answer 2.Thanks, forki
forki23
A: 

Hi,

I tried to implement IUserType for my mapping:

public class MyCustomString : IUserType
{
    public Type ReturnedType
    {
        get { return typeof (MyTable); }
    }

    public SqlType[] SqlTypes
    {
        get { return new[] {NHibernateUtil.String.SqlType}; }
    }    

    public object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        object obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);

        if (obj == null) return null;

        var s = (string) obj;

        if (s == "")
            return null;
        using (ISession session = SessionHelper.OpenSession())
        {
            using (session.BeginTransaction())
            {
                return MyTable.Get(session, s);
            }
        }
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        ((IDataParameter) cmd.Parameters[index]).Value = value == null ? 0 : ((MyTable) value).EntryNo;
    }

   ...
}

and changed the mapping to

    public MyTableMap()
    {
        Id(x => x.EntryNo);

        Map(x => x.Parent).CustomTypeIs<MyCustomString>();

        // References() doesn't allow CustomTypeIs()
        // References(x => x.Parent).CustomTypeIs<MyCustomString>();
    }

This seems to work for my root - but it always opens a session to get the right parent. And it is not lazy - so it always retrieves all parents up to the root :-(

This can't be the right way. I don't want to open a new session - but otherwise I am returning a string and get a runtime type error.

forki23
A: 

Have you considered using the Null Object Pattern instead?

Stefan Moser
I think it's the same problem with the NUll Object pattern. I don't know how to transform the empty string into my NullObject.
forki23
Or should I persist the Null object to the database?
forki23
Oh yes, you're right, you'd get into an infinite loop of the null object contained in the null object contained in the null object . . . aaahhh!!!!
Stefan Moser
+1  A: 

Hi,

I found a (messy) way to get this working:

public class NullEventListener : IPreUpdateEventListener, IPreInsertEventListener, IPreLoadEventListener
{
    #region IPreInsertEventListener Members

    public bool OnPreInsert(PreInsertEvent preInsertEvent)
    {
        var instance = preInsertEvent.Entity as MyTable;
        if (instance == null)
            return false;

        if (instance.Parent == null)
            Set(preInsertEvent.Persister, preInsertEvent.State, "Parent", string.Empty);     

        return false;
    }

    #endregion

    #region IPreLoadEventListener Members

    public void OnPreLoad(PreLoadEvent preLoadEvent)
    {
        var instance = preLoadEvent.Entity as MyTable;
        if (instance == null)
            return;

        try
        {
            // this is really messy!!
            var parent = Get(preLoadEvent.Persister, preLoadEvent.State, "Parent") as MyTable;
            if (parent == null || parent.ID == "")
                throw new Exception("Set to null");
        }
        catch (Exception)
        {
            Set(preLoadEvent.Persister, preLoadEvent.State, "Parent", null);
        }

        return;
    }

    #endregion

    #region IPreUpdateEventListener Members

    public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
    {
        var instance = preUpdateEvent.Entity as MyTable;
        if (instance == null)
            return false;

        if (instance.Parent == null)
            Set(preUpdateEvent.Persister, preUpdateEvent.State, "Parent", string.Empty);     

        return false;
    }

    #endregion

    private static void Set(IEntityPersister persister, object[] state, string propertyName, object value)
    {
        int index = Array.IndexOf(persister.PropertyNames, propertyName);
        if (index == -1)
            return;
        state[index] = value;
    }

    private static object Get(IEntityPersister persister, object[] state, string propertyName)
    {
        int index = Array.IndexOf(persister.PropertyNames, propertyName);
        if (index == -1)
            return null;
        return state[index];
    }
}

Thanks and regards, forki

forki23
+3  A: 

You need to have an IUserType for the primary key column, which does the special NULL value handling.

public MyTableMap()
{
    Id(x => x.EntryNo)
        // Since the PK is a string, it must be assigned by the application.
        .GeneratedBy.Assigned()
        .SetAttribute("type", typeof(SpecialNullValueStringType).AssemblyQualifiedName);

    References(x => x.Parent);
}

public class SpecialNullValueStringType : IUserType
{
    #region IUserType Members
    public bool IsMutable
    {
        get { return false; }
    }

    public Type ReturnedType
    {
        get { return typeof(string); }
    }

    public SqlType[] SqlTypes
    {
        get { return new[] { NHibernateUtil.String.SqlType }; }
    }

    public object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        var obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);

        if (obj == null)
        {
            return null;
        }

        var value = (string) obj;
        if (String.IsNullOrEmpty(value))
        {
            return null;
        }

        return value;
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        if (value == null)
        {
            ((IDataParameter) cmd.Parameters[index]).Value = String.Empty;
        }
        else
        {
            ((IDataParameter) cmd.Parameters[index]).Value = value;
        }
    }

    public object DeepCopy(object value)
    {
        return value;
    }

    public object Replace(object original, object target, object owner)
    {
        return original;
    }

    public object Assemble(object cached, object owner)
    {
        return cached;
    }

    public object Disassemble(object value)
    {
        return value;
    }

    public new bool Equals(object x, object y)
    {
        if (ReferenceEquals(x, y))
        {
            return true;
        }

        if (x == null || y == null)
        {
            return false;
        }

        return x.Equals(y);
    }

    public int GetHashCode(object x)
    {
        return x == null ? typeof(string).GetHashCode() + 473 : x.GetHashCode();
    }
    #endregion
}
Alexander Groß