views:

247

answers:

3

I have a fairly simple class that I want to save to SQL Server via NHibernate (w/ Fluent mappings). The class is made up mostly of optional string fields.

My problem is I default the class fields to string.empty to avoid NullRefExceptions and when NHibernate saves the row to the database each column contains an empty string instead of null.

Question: Is there a way for me to get NHibernate to automatically save null when the string property is an empty string? Or do I need to litter my code with if (string.empty) checks?

+3  A: 

NHibernate is doing what you ask it to do. At one end you are saying that you try to avoid NullReferenceException, at the other end you are trying to save NULL in the database when a value is not null. That sounds to me like a contradiction. Instead of trying to workaround this feature bug, try to either allow nulls (and check the data to prevent the NRE's) or don't allow nulls.

If there's a special case you want to cover with NULL fields vs empty fields, consider reading the correct data (and don't init to String.Empty). If you treat an empty string equal to a null value in that database, just initialize all fields to the empty string to keep it easy and consistent.

Abel
yes... it is doing exactly what I am asking of it... still hoping for an easy way to allow the code to see null as the same as string.empty but for the database to see them as not equal.
Todd
"feature bug"? I'm definitely adding that phrase to my vocabulary. :-)
Jamie Ide
+3  A: 

You can do this with a UserType. I've come to the conclusion that null strings are useless (and a pain in the neck) in my business classes so I convert all nullable string database columns to empty string and vice-versa.

Fleunt usage is:

Map(x => x.MiddleName).CustomType(typeof(NullableString));

/// <summary>
/// UserType for string properties that are database nullable. Using this type
/// will substitue empty string for null when populating object properties
/// and null for empty string in database operations.
/// </summary>
/// <example>
/// Map(x => x.MiddleName).Length(30).Nullable().CustomType(typeof(NullableString));
/// </example>
public class NullableString : IUserType
{
    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.GetHashCode();
    }

    public object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        var valueToGet = NHibernateUtil.String.NullSafeGet(rs, names[0]);
        return valueToGet ?? string.Empty;
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        var stringObject = value as string;
        object valueToSet = string.IsNullOrEmpty(stringObject) ? null : stringObject;
        NHibernateUtil.String.NullSafeSet(cmd, valueToSet, index);
    }

    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 DeepCopy(cached);
    }

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

    public SqlType[] SqlTypes
    {
        get
        {
            return new[] { new SqlType(DbType.String)};
        }
    }

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

    public bool IsMutable
    {
        get { return false; }
    }
}
Jamie Ide
Just a thought, but if null strings are a pain in the neck (agreed!), then why don't you simply make the field non-null and the default value the empty string? Problem solved and no need to write workarounds through code.
Abel
Legacy system, rigid DBA, database not fully under my control, database accessed by other systems, etc.
Jamie Ide
+1  A: 

I wouldn't say that you need to litter your code with checks. I use a single extension method:

public static class StringExtensions
{
    public static string NullIfEmpty(this string s)
    {
        return string.IsNullOrEmpty(s) ? null : s;
    }
}

Then write your entity class this way:

public class MyEntity
{
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value.NullIfEmpty(); }
    }
}

I think it's best that you indicate explicitly that you want this behaviour, because in many cases an empty string might be a valid value in the database.

Using a custom type works too; however, it always feels to me like this "nullifying" behaviour ought to be a behaviour of the entity and not the mapper, and that the entity itself should have a contract that says "I ignore empty strings."

Aaronaught
I see this as being part of the o/r mismatch and therefore the mapper has the responsibility to handle it. I've yet to come up with a valid use case for null strings in a business object, or a need to distinguish between empty string and null in the database. If you're using Oracle, it stores zero length strings as null anyway.
Jamie Ide
@Jamie: I suppose with Oracle that would be true; in SQL Server at least, zero-length strings are different from `NULL`, so it's actually a perfect match to .NET strings, not a mismatch. Zero-length strings are also semantically different from `NULL` - `NULL` means *unknown* or *undefined* whereas a zero-length string means that the value *is* defined but just empty.
Aaronaught
The types are a perfect match, but I'm resolving the o/r mismatch for my objects. And I know what null means, what I'm saying is that the distinction is practically nil for strings. The standard case is that I'm setting a property from user input, so I read the Text property of the control which returns empty string even if the user hasn't touched the control. In order to know that the user meant "undefined/unknown/null", I would have to provide an additional UI element to toggle the text control.
Jamie Ide
@Jamie: I didn't say that it had to be part of the UI logic - that's why I posted the extension method and example above, where the model is responsible for this transformation. I really don't see how the so-called Object-Relational Impedance Mismatch applies at all here. This isn't an Object/Relational mapping problem, it's a Domain/UI mapping problem.
Aaronaught
The UI logic was meant as an example of why null strings rarely (in my experience almost never) have meaning in a real-world domain model. It's hard to be clear in 600 characters. The mismatch is that database string columns can be nullable or non-nullable but .NET strings are always nullable. It's much easier to deal with this mismatch with a user type than handling it throughout the model.
Jamie Ide