Dienstag 26. Februar 2008 von ReneMT
In letzer Zeit setze ich bei Webprojekten vermehrt Castle ActiveRecord ein, das ja bekanntlich auf NHibernate basiert. An sich eine super Sache, ActiveRecord (respektive NHibernate) ist wirklich ein ziemlich komfortabler O/R-Mapper, der wahre Produktivitäts-Sprünge beschert.
Allerdings gibt es ein kleines Problem bei der Zusammenarbeit von NHibernate und MySQL im Zusammenhang mit dem MySQL Connector/NET: MySQL bietet, warum auch immer, so genannte “Null-DateTimes” an, d.h. Daten der Art 00/00/0000. Außer MySQL kann damit allerdings niemand so recht etwas anfangen, schon gar nicht das System.DateTime-Struct von .NET. Die Eigenintelligenz von NHibernate gibt sich nun zwar alle Mühe, den Inhalt eines nicht-leeres DATE(TIME)-Feld in der DB in ein DateTime zu konvertieren, erntet aber leider eine Exception, da .NET, verständlicher Weise, keine Null-DateTimes akzeptiert.
Eine in meinen Augen schlaue Lösung wäre, die Null-DateTimes wie ein leeres Feld (NULL) zu behandel und dementsprechend statt eines normalen DateTimes ein DateTime?, also ein nullable DateTime für die entsprechende Porperty zu benutzen. Was nun noch fehlt ist die Möglichkeit, das Feld in der Datenbank ohne Exception entsprechend zu konvertieren, d.h. die Intelligenz von NHibernate durch einen eigenen “Konverter” zu erweitern.
Genau für solche Zwecke bietet NHibernate den IUserType an. Eine Klasse, die dieses Interface implementiert, kann für die entsprechenden Properties einer O/R-Klasse als ColumnType angegeben werden. Das Lesen und Schreiben der Property, bzw. dessen Vorbereitung, erledigt dann die IUserType-Implementierung. Im folgenden nun ein Beispiel für eine geeignete Vorgehensweise bezüglich der MySqlDateTime-Problematik:
UserType:
namespace ReneMt.Data
{
public class HibernateNullableDateTime : NHibernate.UserTypes.IUserType
{
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object DeepCopy(object value)
{
return value;
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public new bool Equals(object x, object y)
{
if (null == x && null == y)
return true;
else if (null != x)
return x.Equals(y);
else
return y.Equals(x);
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public bool IsMutable
{
get { return false; }
}
public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)
{
if (names.Length == 1)
{
object obj = rs[names[0]];
if (null == obj || obj is DBNull)
return new DateTime?();
if (obj is MySqlDateTime)
{
MySqlDateTime mySqlDateTime = (MySqlDateTime)obj;
if (mySqlDateTime.IsNull)
return new DateTime?();
else
{
if (0 == mySqlDateTime.Day || 0 == mySqlDateTime.Month || 0 == mySqlDateTime.Year)
return new DateTime?();
return new DateTime?(mySqlDateTime.GetDateTime());
}
}
else if (obj is DateTime)
return new DateTime?((DateTime)obj);
throw new InvalidCastException("Can not convert object of type " +
obj.GetType().FullName + " to " + typeof(MySqlDateTime?).FullName);
}
throw new InvalidCastException("Single column expected.");
}
public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)
{
System.Data.IDataParameter param = cmd.Parameters[index] as System.Data.IDataParameter;
param.DbType = System.Data.DbType.DateTime;
if (null == value)
{
param.Value = DBNull.Value;
}
else if (value is MySqlDateTime)
{
MySqlDateTime mySqlDateTime = (MySqlDateTime)value;
if (mySqlDateTime.IsNull)
param.Value = DBNull.Value;
else
param.Value = mySqlDateTime.GetDateTime();
}
else if (value is DateTime)
{
param.Value = value;
}
throw new ArgumentException("Object of type " + typeof(MySqlDateTime).FullName +
" expected, but object of type " + value.GetType().FullName + " has been passed.");
}
public object Replace(object original, object target, object owner)
{
return original;
}
public Type ReturnedType
{
get
{
return typeof(DateTime?);
}
}
public NHibernate.SqlTypes.SqlType[] SqlTypes
{
get
{
return new NHibernate.SqlTypes.SqlType[] { NHibernate.NHibernateUtil.DateTime.SqlType };
}
}
}
}
O/R-Klasse:
using Castle.ActiveRecord;
namespace ReneMt.Data
{
[ActiveRecord("TEST")]
public class NaturalPerson : ActiveRecordBase
{
private int m_id;
private DateTime? m_dayOfBirth;
[Property("dayofbirth",
ColumnType = "ReneMt.Data.HibernateNullableDateTime, ReneMt.Data")]
public DateTime? DayOfBirth
{
get { return m_dayOfBirth; }
set { m_dayOfBirth = value; }
}
}
}
That’s it