I had to deal with this myself. The project never panned out, so I'm not sure how fool-proof the plan was, but it seemed to work.
http://forum.hibernate.org/viewtopic.php?t=978393
However, the type system has changed a bit since then, if I recall. I took that code and hacked it up until I got something that seemed to pass my tests. Here was my result...
Code:
namespace JuddNet.Data.FauxNullTypes.Identifier
{
/// <summary>
/// see http://forum.hibernate.org/viewtopic.php?t=935678&highlight
/// http://forums.hibernate.org/viewtopic.php?t=933785
/// http://forum.hibernate.org/viewtopic.php?t=978393
/// </summary>
[Serializable]
public class ZeroAsNullDecimalIdentifierType : AbstractType, IIdentifierType
{
// Has to inherite from AbstractType because we need to override NullSafeSet(),
// which is not exposed in anything "lower" than AbstractType
private const decimal ZERO = 0;
#region IIdentifierType Members
/// <summary>
///
/// </summary>
/// <param name="xml"></param>
/// <returns></returns>
public object StringToObject(string xml)
{
object result = FromString(xml);
if (result != null && ZERO.Equals((decimal)result))
{
result = null;
}
return result;
}
public object FromStringValue(string xml)
{
// This method cannot return null
return decimal.Parse(xml);
}
#endregion
public override object DeepCopy(object val)
{
// val would be an decimal, 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 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(IMapping mapping)
{
return 1;
}
public override bool HasNiceEquals
{
get { return true; }
}
public override bool IsMutable
{
get { return false; }
}
public override string Name
{
get { return "ZeroAsNullDecimalIdentifierType"; }
}
public override sealed object NullSafeGet(IDataReader rs, string name, ISessionImplementor session, object owner)
{
// Copied from NullableType
return NullSafeGet(rs, name);
}
public override object NullSafeGet(IDataReader rs, string[] names, 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
{
decimal fieldValue = Convert.ToDecimal(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, ISessionImplementor session)
{
NullSafeSet(cmd, value, index, session);
}
public override void NullSafeSet(IDbCommand cmd, object value, int index, ISessionImplementor session)
{
IDataParameter parm = cmd.Parameters[index] as IDataParameter;
parm.Value = value ?? ZERO;
}
public override Type ReturnedClass
{
get { return typeof(decimal); }
}
public override SqlType[] SqlTypes(IMapping mapping)
{
return new SqlType[] {
SqlTypeFactory.Decimal
};
}
public override string ToLoggableString(object value, ISessionFactoryImplementor factory)
{
if (value != null)
{
return value.ToString();
}
else
{
return "null-decimal";
}
}
public override bool IsDirty(object old, object current, bool[] checkable, 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 IsDirty(old, current, session);
}
}
}
For your entity that is your aggregate (which I believe in your case is Person), you would use this type for the identifier type...
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Domain">
<class name="Person" table="PersonTable">
<id
name="Id"
column="PersonId"
type="JuddNet.Data.FauxNullTypes.Identifier.ZeroAsNullDecimalIdentifierType, JuddNet.Data">
<generator class="assigned"/>
</id>
<property name="Name" column="CNme" type="string" length="30"/>
</class>
</hibernate-mapping>
Here's what I had in the entity where Person is an aggregate...
Code:
<many-to-one
name="Person" column="PersonFk"
class="Domain.Person"
not-null="false" not-found="ignore"/>
Hope this at least gets you started.