I wrote a user type to get around it. Source code is at the bottom. This is only for int32 type, but you should be able to easily adapt it for other types. You use it on the <id> element of the one-side of the relationship (rather than the <many-to-one> element). For example:
Code:
<class name="...">
<id name="ID"
column="id"
type="PatientSys.Data.UserTypes.ZeroAsNullInt32IdentifierType, PatientSys.Data"
unsaved-value="0"
>
<generator class="identity" />
</id>
</class>
Code for the user type:Code:
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using NHibernate;
using NHibernate.Type;
using NHibernate.SqlTypes;
namespace PatientSys.Data.UserTypes
{
/// <summary>
/// see http://forum.hibernate.org/viewtopic.php?t=935678&highlight
/// http://forums.hibernate.org/viewtopic.php?t=933785
/// </summary>
[Serializable]
public class ZeroAsNullInt32IdentifierType : AbstractType, IIdentifierType
{
// Has to inherite from AbstractType because we need to override NullSafeSet(),
// which is not exposed in anything "lower" than AbstractType
private const int ZERO = 0;
public ZeroAsNullInt32IdentifierType() { }
#region IIdentifierType Members
/// <summary>
///
/// </summary>
/// <param name="xml"></param>
/// <returns></returns>
public object StringToObject(string xml)
{
// This method can return null (does so in Int32Type)
object result = FromString(xml);
if (result != null && ZERO.Equals((int)result))
{
result = null;
}
return result;
}
public object FromStringValue(string xml)
{
// This method cannot return null
return int.Parse(xml);
}
#endregion
public override object DeepCopy(object val)
{
// val would be an int, so just returning it would return a copy
return val;
}
public override bool Equals(object x, object y)
{
object xTransformed = ((x == null) || ZERO.Equals(x)) ? ZERO : x;
object yTransformed = ((y == null) || ZERO.Equals(y)) ? ZERO : y;
return NHibernate.Util.ObjectUtils.Equals(xTransformed, yTransformed);
}
/// <include file='IType.cs.xmldoc'
/// path='//members[@type="IType"]/member[@name="M:IType.FromString"]/*'
/// />
/// <remarks>
/// <para>
/// This implementation forwards the call to <see cref="FromStringValue"/> if the parameter
/// value is not empty.
/// </para>
/// <para>
/// It has been "sealed" because the Types inheriting from <see cref="NullableType"/>
/// do not need and should not override this method. All of their implementation
/// should be in <see cref="FromStringValue"/>.
/// </para>
/// </remarks>
public override sealed object FromString(string xml)
{
// Copied from NullableType
return (xml == null || xml.Length == 0) ? null : FromStringValue(xml);
}
public override int GetColumnSpan(NHibernate.Engine.IMapping mapping)
{
return 1;
}
public override bool HasNiceEquals
{
get { return true; }
}
public override bool IsMutable
{
get { return false; }
}
public override string Name
{
get { return "ZeroAsNullInt32IdentifierType"; }
}
public override sealed object NullSafeGet(IDataReader rs, string name, NHibernate.Engine.ISessionImplementor session, object owner)
{
// Copied from NullableType
return NullSafeGet(rs, name);
}
public override object NullSafeGet(IDataReader rs, string[] names, NHibernate.Engine.ISessionImplementor session, object owner)
{
// Copied from NullableType
return NullSafeGet(rs, names[0]);
}
public virtual object NullSafeGet(IDataReader rs, string name)
{
int index = rs.GetOrdinal(name);
if (rs.IsDBNull(index))
{
return null;
}
else
{
int fieldValue = Convert.ToInt32(rs[index]);
// If it is zero in the database, interpret that as null
return ZERO.Equals(fieldValue) ? null : (object)fieldValue;
}
}
public override void NullSafeSet(IDbCommand cmd, object value, int index, bool[] settable, NHibernate.Engine.ISessionImplementor session)
{
NullSafeSet(cmd, value, index, session);
}
public override void NullSafeSet(IDbCommand cmd, object value, int index, NHibernate.Engine.ISessionImplementor session)
{
// TODO: is this override even needed now that we don't write a zero for null into the database?
IDataParameter parm = cmd.Parameters[index] as IDataParameter;
parm.Value = value == null ? DBNull.Value : value;
}
public override Type ReturnedClass
{
get { return typeof(Int32); }
}
public override SqlType[] SqlTypes(NHibernate.Engine.IMapping mapping)
{
return new SqlType[] {
//new Int32SqlType()
SqlTypeFactory.Int32
};
}
[Obsolete("This method was renamed ToLoggableString() from NHibernate 1.0 to 1.2")]
public string ToString(object value, NHibernate.Engine.ISessionFactoryImplementor factory)
{
return ToLoggableString(value, factory);
}
public override string ToLoggableString(object value, NHibernate.Engine.ISessionFactoryImplementor factory)
{
// TODO: this may cause a problem, as a null value may come in; but I don't know if it will.
return value.ToString();
}
public override bool IsDirty(object old, object current, bool[] checkable, NHibernate.Engine.ISessionImplementor session)
{
// TODO: this overload of IsDirty is new in NHibernate 1.2. Don't know what the "checkable"
// parameter is, so we just call the other IsDirty for now.
return this.IsDirty(old, current, session);
}
}
}