SQL 2008 HierarchyID support in NHibernate - nhibernate

Searched various NHibernate lists and haven't come up with a definitive answer. The SQL2008 dialect doesn't appear to have support for the HierarchyID data type - new date and time types only.
Does anyone have a good implementation or an effective workaround? I'd really like to leverage HierarchyID in a new app of mine. Support for this interesting and powerful data type is sorely lacking in MS's own tools so I'm not shocked that NHibernate doesn't have support.
There are some approaches out there that I haven't delved into yet. Wondering if anyone has some experience in what works, what is more performant, etc.'
Full disclosure: I'm working with Castle ActiveRecord but this seems like an NHibernate issue.

I've given Needles' answer a test run. It's a very good answer but there are some changes needed to make it function (at least in .NET 4). Here's what I've come up with for my project:
Update: the following code can be download over at GitHub and will be updated there. NHiberntate.HierarchyId.UserType
SqlHierarchyId IUserType
namespace NHibernate.UserTypes
{
using SqlTypes;
using System;
using System.Data;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Types;
public class HierarchyId : IUserType
{
#region Properties
public SqlType[] SqlTypes
{
get { return new[] { NHibernateUtil.String.SqlType }; }
}
public Type ReturnedType
{
get { return typeof(SqlHierarchyId); }
}
public bool IsMutable
{
get { return true; }
}
#endregion Properties
#region Methods
new public 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)
{
object prop1 = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (prop1 == null) return null;
return SqlHierarchyId.Parse(new SqlString(prop1.ToString()));
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if (value == null)
((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
else if (value is SqlHierarchyId)
((IDataParameter)cmd.Parameters[index]).Value = ((SqlHierarchyId)value).ToString();
}
public object DeepCopy(object value)
{
if (value == null) return null;
return SqlHierarchyId.Parse(((SqlHierarchyId)value).ToString());
}
public object Replace(object original, object target, object owner)
{
return DeepCopy(original);
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
#endregion Methods
}
}
Mapping
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="DataLayer" namespace="NHibernate.Map">
<class name="NHibernate.Map.OrganizationUnit, DataLayer" table="`orgunit`">
<property name="HierarchyId" column="`ou_hid`" type="NHibernate.UserTypes.HierarchyId, DataLayer" />
...
</class>
</hibernate-mapping>
Object with HierarchyId
namespace NHibernate.Map
{
using Microsoft.SqlServer.Types;
public class OrganizationUnit
{
#region Fields
private SqlHierarchyId _hierarchyId;
...
#endregion Fields
#region Properties
public virtual SqlHierarchyId HierarchyId
{
get { return _hierarchyId; }
set { _hierarchyId = value; }
}
...
#endregion Properties
}
}

Disclaimer: Im not an NHibernate expert, however, we are using it with Fluent in an upcoming project which uses SQL Server 2008 R2 and Hierarchy IDs. The code below is what we are using currently on our dev environment and is not fully tested/refined. I copied the majority of the code from elsewhere (sorry I lost the link!)
You need to create a User Defined Type and then use it in your mappings. The mapping below is Fluent, Im not aware how to do it using ActiveRecord but Im guessing it should be similar!
User Defined Type
namespace YourNamespace {
public class SqlHierarchyIdUserType : IUserType {
public 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) {
object prop1 = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if(prop1 == null)
return null;
return SqlHierarchyId.Parse(new SqlString(prop1.ToString()));
}
public void NullSafeSet(IDbCommand cmd, object value, int index) {
if(value == null) {
((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
} else {
if(value is SqlHierarchyId) {
SqlHierarchyId hId = (SqlHierarchyId)value;
((IDataParameter)cmd.Parameters[index]).Value = hId.ToString();
}
}
}
public object DeepCopy(object value) {
if(value == null)
return null;
var sourceTarget = (SqlHierarchyId)value;
SqlHierarchyId copy = SqlHierarchyId.Parse(sourceTarget.ToString());
return copy;
}
public object Replace(object original, object target, object owner) {
return DeepCopy(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[] { NHibernateUtil.String.SqlType }; }
}
public Type ReturnedType {
get { return typeof(SqlHierarchyId); }
}
public bool IsMutable {
get { return true; }
}
}
}
Fluent Mapping
Map(e => e.YourSqlHierarchyIdProperty)
.Column("YourSqlHierarchyIdFieldName")
.CustomType<SqlHierarchyIdUserType>();
Reading this post:
Castle ActiveRecord: Map to IUserType wihtin Class in C#
ActiveRecord uses a [Property] attribute to map User Defined Types. So for you it would look something like this:
public class YourDataObject {
[Property(ColumnType="YourNamespace.SqlHierarchyIdUserType, YourNamespace")
public virtual SqlHierarchyId YourSqlHierarchyIdProperty;
}
Hope it helps!

Related

Map SQLDecimal property in NHibernate

I'm trying to read a decimal (38,16) from a SQL Server DB and struggling. After much reading around I'm trying to implement a custom type for SQL Decimal with the following code:
public class BigDecimal : IUserType
{
public bool Equals(object x, object y)
{
return object.Equals(x,y);
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
int index = rs.GetOrdinal(names[0]);
object result = rs.GetValue(index);
return result;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
//Not got here yet
}
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 cached;
}
public object Disassemble(object value)
{
return value;
}
public SqlType[] SqlTypes { get { return new[] {SqlTypeFactory.Decimal}; } }
public Type ReturnedType { get { return typeof (SqlDecimal); } }
public bool IsMutable { get { return false; } }
}
but the output of rs.GetValue is a decimal not at SQLDecimal which causes an OverflowException.
The class looks like this:
public class Billy
{
public BigDecimal TheNumber {get;set;}
}
and the mapping looks like this:
public class BillyMap : ClassMap<Billy>
{
public BillyMap()
{
Map(b=>b.TheNumber).CustomType<BigDecimal>();
}
}
Please can someone tell me where I'm going wrong.
I think you need to cast the reader to SqlDataReader so you can access either GetSqlDecimal() or GetSqlValue(), as GetValue() will convert to a basic .Net Framework type. From 'https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqldatareader.getsqlvalue%28v=vs.110%29.aspx':
GetSqlValue returns data using the native SQL Server types. To retrieve data using the .NET Framework types, see GetValue.
In the end I made a something which performs a convert in the SQL and made it a Property Part and just use it on all the mapping files:
private const string DECIMAL_CONVERSION = "(CONVERT(decimal(28,6), [{0}]))";
private static string MapDecimalProperty(string fieldName)
{
return string.Format(DECIMAL_CONVERSION, fieldName.Trim('[',']'));
}
public static PropertyPart LongDecimal(this PropertyPart part, string fieldName)
{
return part.Formula(MapDecimalProperty(fieldName));
}
the on the mapping:
Map(ep => ep.BigDecimalField).EDWDecimal("[BigDecimalField]");
this works for now. I've informed the data architecture team that this is happening and they don't think that it will be a problem with any current data and will consider it for future developments.

Converting string to double in fluent-nhibernate mapping

I have a sql table with a string column that could contain null, empty string or a double value.
I want to map this column to a C# property that is a double, defaulting to zero when the column is null or empty.
Can I do this with a fluent-nhibernate mapping?
I tried this:
Map(p => p.doubleProperty).CustomSqlType("varchar(20)").CustomType("double").Default("0");
and variations on this theme but I always get an error that the conversion failed.
For now I've gone with a custom type which allows me to use
Map(p=>p.doubleProperty).CustomType<DoubleString>();
Which will do for my current needs.
I'll leave the question open for now in case someone comes up with an easier solution.
Code for the DoubleString type is below.
public class DoubleString : 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]) as string;
double returnValue = 0.0;
double.TryParse(valueToGet, out returnValue);
return returnValue;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
object valueToSet = ((double)value).ToString();
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(double); }
}
public bool IsMutable
{
get { return true; }
}
}
I would just map this to a string and in your entity have a property that is a double that does the conversion. Seems easier and cleaner than doing it in the mapping.
Maybe something like this:
public double Price
{
get
{
double price = -1.0;
Double.TryParse(stringProperty, out price);
return price;
}
set { stringProperty = value.ToString(); }
}
I solved kind of the same problem using two Properties which both referred to the same private variable. In my example the database contains a varchar which is NOT USED or ACTUAL, i therefor wanted a bool in my application.
private bool _myBool= true;
public virtual bool MyBool{ get { return _myBool; } set { _myBool= value; } }
public virtual string MyString
{
get { return _myBool ? "ACTUAL" : "NOT USED"; }
set { _myBool= value.Equals("NOT USED"); }
}
}

NHibernate: Using a custom type as a primary key

I have a legacy schema that contains tables with primary keys of type binary(16) -- its a MD5 hash of the other columns. NHibernate does not work with byte[] as a key since it does not implement Equals so I wrapped this in a custom type and provided NHibernate with an implementation of IUserType. Notice that MD5Hash is a struct and not a class.
public struct MD5Hash : IComparable, IComparable<MD5Hash>, IEquatable<MD5Hash> {
private readonly byte[] contents;
...
}
Everything worked fine until I created a many-to-one mapping to a type that uses MD5Hash as its key.
public class Referenced : IEquatable<Referenced> {
...
public virtual MD5Hash Id { get; set; }
public virtual string Name { get; set; } // must NOT be null
...
}
public class Referencer : IEquatable<Referencer> {
...
public virtual MD5Hash Id { get; set; }
public virtual Referenced Other { get; set } // may be null
...
}
When I attempt to load objects of type Referencer, NHibernate does not see a null value for the key when the row contains a NULL value so it attempts to instantiate an object of type
Referenced, assign it to Referencer, and update Referencer in the database. Since Referenced has a property, Name, which maps to a non-nullable column, NHibernate raises an exception. What I want is for NHibernate to set the Other property to null.
I could change the definition of MD5Hash to be a class instead of a struct but I have an unknown number of places in the code that probably assumes MD5Hash can never be null so I am looking for another solution.
The code for the custom type...
internal class MD5HashType : IUserType {
public SqlType[] SqlTypes {
get { return new[] { new SqlType(DbType.Binary, 16) }; }
}
public Type ReturnedType {
get { return typeof(MD5Hash); }
}
public new bool Equals(object x, object y) {
return Object.Equals(x, y);
}
public int GetHashCode(object x) {
return (null == x) ? 0 : x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner) {
var val = NHibernateUtil.Binary.NullSafeGet(rs, names[0]);
return (null == val || DBNull.Value == val) ? MD5Hash.Empty : new MD5Hash((byte[])val);
}
public void NullSafeSet(IDbCommand cmd, object value, int index) {
var val = (MD5Hash.Empty == ((MD5Hash)value)) ? null : ((MD5Hash)value).ToByteArray();
NHibernateUtil.Binary.NullSafeSet(cmd, val, index);
}
public object DeepCopy(object value) {
return value;
}
public bool IsMutable {
get { return false; }
}
public object Replace(object original, object target, object owner) {
return original;
}
public object Assemble(object cached, object owner) {
return cached;
}
public object Disassemble(object value) {
return value;
}
}
The problem appears to be that NHibernate can't tell that MD5Hash.Empty means no value. Have you tried creating custom event listeners such as the following to handle this? Something like:
public class CustomLoadListener : DefaultLoadEventListener {
public override void OnLoad(LoadEvent #event, LoadType loadType) {
if(#event.EntityId is MD5Hash) {
var id = (MD5Hash) #event.EntityId;
if(id == MD5Hash.Empty) {
#event.Result = new Referenced { Id = MD5Hash.Empty };
return;
}
}
base.OnLoad(#event, loadType);
}
}
public class CustomSaveOrUpdateListener : DefaultSaveOrUpdateEventListener {
public override void OnSaveOrUpdate(SaveOrUpdateEvent #event) {
var entity = #event.Entity as Referenced;
if(entity != null && entity.Id == MD5Hash.Empty) {
return;
}
base.OnSaveOrUpdate(#event);
}
}
You would then have to configure these listeners in your session factory via hibernate.cfg.xml:
<session-factory>
<!-- various properties -->
<listener type="load" class="NhHacking.CustomLoadListener, NhHacking"/>
<listener type="save-update" class="NhHacking.CustomSaveOrUpdateListener, NhHacking"/>
</session-factory>
If someone has a better idea of how to accomplish this, I would love to hear it.

Unit testing NHibernate w/ SQLite and DateTimeOffset mappings

Porting over an application to use NHibernate from a different ORM.
I've started to put in place the ability to run our unit tests against an in memory SQLite database. This works on the first few batches of tests, but I just hit a snag. Our app would in the real world be talking to a SQL 2008 server, and as such, several models currently have a DateTimeOffset property. When mapping to/from SQL 2008 in non-test applications, this all works fine.
Is there some mechanism either in configuring the database or some other facility so that when I use a session from my SQLite test fixture that the DateTimeOffset stuff is "auto-magically" handled as the more platform agnostic DateTime?
Coincidentally, I just hit this problem myself today :) I haven't tested this solution thoroughly, and I'm new to NHibernate, but it seems to work in the trivial case that I've tried.
First you need to create an IUserType implementation that will convert from DateTimeOffset to DateTime. There's a full example of how to create a user type on the Ayende blog but the relevant method implementations for our purposes are:
public class NormalizedDateTimeUserType : IUserType
{
private readonly TimeZoneInfo databaseTimeZone = TimeZoneInfo.Local;
// Other standard interface implementations omitted ...
public Type ReturnedType
{
get { return typeof(DateTimeOffset); }
}
public SqlType[] SqlTypes
{
get { return new[] { new SqlType(DbType.DateTime) }; }
}
public object NullSafeGet(IDataReader dr, string[] names, object owner)
{
object r = dr[names[0]];
if (r == DBNull.Value)
{
return null;
}
DateTime storedTime = (DateTime)r;
return new DateTimeOffset(storedTime, this.databaseTimeZone.BaseUtcOffset);
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if (value == null)
{
NHibernateUtil.DateTime.NullSafeSet(cmd, null, index);
}
else
{
DateTimeOffset dateTimeOffset = (DateTimeOffset)value;
DateTime paramVal = dateTimeOffset.ToOffset(this.databaseTimeZone.BaseUtcOffset).DateTime;
IDataParameter parameter = (IDataParameter)cmd.Parameters[index];
parameter.Value = paramVal;
}
}
}
The databaseTimeZone field holds a TimeZone which describes the time zone that is used to store values in the database. All DateTimeOffset value are converted to this time zone before storage. In my current implementation it is hard-coded to the local time zone, but you could always define an ITimeZoneProvider interface and have it injected into a constructor.
To use this user type without modifying all my class maps, I created a Convention in Fluent NH:
public class NormalizedDateTimeUserTypeConvention : UserTypeConvention<NormalizedDateTimeUserType>
{
}
and I applied this convention in my mappings, as in this example (the new NormalizedDateTimeUserTypeConvention() is the important part):
mappingConfiguration.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())
.Conventions.Add(
PrimaryKey.Name.Is(x => x.EntityType.Name + "Id"),
new NormalizedDateTimeUserTypeConvention(),
ForeignKey.EndsWith("Id"));
Like I said, this isn't tested thoroughly, so be careful! But now, all I need to do is to alter one line of code (the fluent mappings specification) and I can switch between DateTime and DateTimeOffset in the database.
Edit
As requested, the Fluent NHibernate configuration:
To build a session factory for SQL Server:
private static ISessionFactory CreateSessionFactory(string connectionString)
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString))
.Mappings(m => MappingHelper.SetupMappingConfiguration(m, false))
.BuildSessionFactory();
}
For SQLite:
return Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => MappingHelper.SetupMappingConfiguration(m, true))
.ExposeConfiguration(cfg => configuration = cfg)
.BuildSessionFactory();
Implementation of SetupMappingConfiguration:
public static void SetupMappingConfiguration(MappingConfiguration mappingConfiguration, bool useNormalizedDates)
{
mappingConfiguration.FluentMappings
.AddFromAssembly(Assembly.GetExecutingAssembly())
.Conventions.Add(
PrimaryKey.Name.Is(x => x.EntityType.Name + "Id"),
ForeignKey.EndsWith("Id"));
if (useNormalizedDates)
{
mappingConfiguration.FluentMappings.Conventions.Add(new NormalizedDateTimeUserTypeConvention());
}
}
Another proposal which allow to keep track of the original timezone offset:
public class DateTimeOffsetUserType : ICompositeUserType
{
public string[] PropertyNames
{
get { return new[] { "LocalTicks", "Offset" }; }
}
public IType[] PropertyTypes
{
get { return new[] { NHibernateUtil.Ticks, NHibernateUtil.TimeSpan }; }
}
public object GetPropertyValue(object component, int property)
{
var dto = (DateTimeOffset)component;
switch (property)
{
case 0:
return dto.UtcTicks;
case 1:
return dto.Offset;
default:
throw new NotImplementedException();
}
}
public void SetPropertyValue(object component, int property, object value)
{
throw new NotImplementedException();
}
public Type ReturnedClass
{
get { return typeof(DateTimeOffset); }
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, null) && ReferenceEquals(y, null))
return true;
if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader dr, string[] names, ISessionImplementor session, object owner)
{
if (dr.IsDBNull(dr.GetOrdinal(names[0])))
{
return null;
}
var dateTime = (DateTime)NHibernateUtil.Ticks.NullSafeGet(dr, names[0], session, owner);
var offset = (TimeSpan)NHibernateUtil.TimeSpan.NullSafeGet(dr, names[1], session, owner);
return new DateTimeOffset(dateTime, offset);
}
public void NullSafeSet(IDbCommand cmd, object value, int index, ISessionImplementor session)
{
object utcTicks = null;
object offset = null;
if (value != null)
{
utcTicks = ((DateTimeOffset)value).DateTime;
offset = ((DateTimeOffset)value).Offset;
}
NHibernateUtil.Ticks.NullSafeSet(cmd, utcTicks, index++, session);
NHibernateUtil.TimeSpan.NullSafeSet(cmd, offset, index, session);
}
public object DeepCopy(object value)
{
return value;
}
public bool IsMutable
{
get { return false; }
}
public object Disassemble(object value, ISessionImplementor session)
{
return value;
}
public object Assemble(object cached, ISessionImplementor session, object owner)
{
return cached;
}
public object Replace(object original, object target, ISessionImplementor session, object owner)
{
return original;
}
}
Fluent NNibernate convention from DateTimeOffset ICompositeUserType would be:
public class DateTimeOffsetTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => x.Type == typeof(DateTimeOffset));
}
public void Apply(IPropertyInstance instance)
{
instance.CustomType<DateTimeOffsetUserType>();
}
}
As i'm short on rep I can not add this as a comment to the accepted answer, but wanted to add some additional information I found while implementing the solution in the accepted answer. I too was getting the error that the dialect doesn't support DateTimeOffset when calling schema export. After adding in log4net logging support, I was able to figure out that my properties that were of type DateTimeOffset? were not being handled by the convention. That is, the convention wasn't being applied to nullable DateTimeOffset properties.
To solve this I created a class which derrives from NormalizedDateTimeUserType and overrides the ReturnedType property (had to mark the original as virtual). Then I created a second UserTypeConvention for my derrived class, and finally added the second convention to my configuration.

Mapping empty strings to NULL in NHibernate

I have a SQL Server DB with a recursive table:
MyTable:
ID : string PrimaryKey
Parent: string references MyTable - NOTNULL !!
and map with Fluent NHibernate to
class MyTable
{
public virtual string ID {get; set;}
public virtual MyTable Parent {get; set;}
}
My problem is that Parent should be null in my C# app if the column Parent is "" (empty string) in the database and vice versa. Unfortunately I can't change the column type to accept NULL!
I tried to use IEmptyInterceptor but I don't get it working.
Thanks in advance,
forki
You need to have an IUserType for the primary key column, which does the special NULL value handling.
public MyTableMap()
{
Id(x => x.EntryNo)
// Since the PK is a string, it must be assigned by the application.
.GeneratedBy.Assigned()
.SetAttribute("type", typeof(SpecialNullValueStringType).AssemblyQualifiedName);
References(x => x.Parent);
}
public class SpecialNullValueStringType : IUserType
{
#region IUserType Members
public bool IsMutable
{
get { return false; }
}
public Type ReturnedType
{
get { return typeof(string); }
}
public SqlType[] SqlTypes
{
get { return new[] { NHibernateUtil.String.SqlType }; }
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null)
{
return null;
}
var value = (string) obj;
if (String.IsNullOrEmpty(value))
{
return null;
}
return value;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if (value == null)
{
((IDataParameter) cmd.Parameters[index]).Value = String.Empty;
}
else
{
((IDataParameter) cmd.Parameters[index]).Value = value;
}
}
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 cached;
}
public object Disassemble(object value)
{
return value;
}
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 == null ? typeof(string).GetHashCode() + 473 : x.GetHashCode();
}
#endregion
}
I found a (messy) way to get this working:
public class NullEventListener : IPreUpdateEventListener, IPreInsertEventListener, IPreLoadEventListener
{
#region IPreInsertEventListener Members
public bool OnPreInsert(PreInsertEvent preInsertEvent)
{
var instance = preInsertEvent.Entity as MyTable;
if (instance == null)
return false;
if (instance.Parent == null)
Set(preInsertEvent.Persister, preInsertEvent.State, "Parent", string.Empty);
return false;
}
#endregion
#region IPreLoadEventListener Members
public void OnPreLoad(PreLoadEvent preLoadEvent)
{
var instance = preLoadEvent.Entity as MyTable;
if (instance == null)
return;
try
{
// this is really messy!!
var parent = Get(preLoadEvent.Persister, preLoadEvent.State, "Parent") as MyTable;
if (parent == null || parent.ID == "")
throw new Exception("Set to null");
}
catch (Exception)
{
Set(preLoadEvent.Persister, preLoadEvent.State, "Parent", null);
}
return;
}
#endregion
#region IPreUpdateEventListener Members
public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
{
var instance = preUpdateEvent.Entity as MyTable;
if (instance == null)
return false;
if (instance.Parent == null)
Set(preUpdateEvent.Persister, preUpdateEvent.State, "Parent", string.Empty);
return false;
}
#endregion
private static void Set(IEntityPersister persister, object[] state, string propertyName, object value)
{
int index = Array.IndexOf(persister.PropertyNames, propertyName);
if (index == -1)
return;
state[index] = value;
}
private static object Get(IEntityPersister persister, object[] state, string propertyName)
{
int index = Array.IndexOf(persister.PropertyNames, propertyName);
if (index == -1)
return null;
return state[index];
}
}
Thanks and regards,
forki
I'd go for a IUserType which would convert empty string to nulls and vice versa. Two methods to pay attention to are NullSafeGet and NullSafeSet.
Not sure though how custom types integrate with Fluent NHibernate.
I tried to implement IUserType for my mapping:
public class MyCustomString : IUserType
{
public Type ReturnedType
{
get { return typeof (MyTable); }
}
public SqlType[] SqlTypes
{
get { return new[] {NHibernateUtil.String.SqlType}; }
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
object obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null) return null;
var s = (string) obj;
if (s == "")
return null;
using (ISession session = SessionHelper.OpenSession())
{
using (session.BeginTransaction())
{
return MyTable.Get(session, s);
}
}
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
((IDataParameter) cmd.Parameters[index]).Value = value == null ? 0 : ((MyTable) value).EntryNo;
}
...
}
and changed the mapping to
public MyTableMap()
{
Id(x => x.EntryNo);
Map(x => x.Parent).CustomTypeIs<MyCustomString>();
// References() doesn't allow CustomTypeIs()
// References(x => x.Parent).CustomTypeIs<MyCustomString>();
}
This seems to work for my root - but it always opens a session to get the right parent.
And it is not lazy - so it always retrieves all parents up to the root :-(
This can't be the right way. I don't want to open a new session - but otherwise I am returning a string and get a runtime type error.
Have you considered using the Null Object Pattern instead?