How to have NHibernate persist a String.Empty prop

2019-07-17 03:53发布

问题:

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?

回答1:

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.



回答2:

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.

Fluent 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; }
    }
}


回答3:

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."