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.
Related
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.
Based on Jimmy's Enumeration classes idea I am wanting to see if I can avoid using the constructor to instantiate my type (which I assume is happening with the discriminator-value) but rather use a "factory method"-esque way of getting my instance mapped from the db.
Here is my type:
public class Impact : Enumeration
{
public static readonly Impact Carbon
= new Impact(1, "Carbon dioxide equivalent", CommonUnit.CO2e);
public static readonly Impact Energy
= new Impact(2, "Energy", CommonUnit.MJ);
public static readonly Impact Cost
= new Impact(3, "Cost", CommonUnit.Dollars);
public Impact(int index, string name, CommonUnit unit)
: base(index, name)
{
this.Unit = unit;
}
public CommonUnit Unit { get; private set; }
}
And here is the definition for Enumeration:
public class Enumeration : ValueObject
{
public Enumeration(int index, string displayName)
{
this.Index = index;
this.DisplayName = displayName;
}
public int Index { get; private set; }
public string DisplayName { get; private set; }
public override string ToString()
{
return this.DisplayName;
}
public static IEnumerable<T> GetAllFor<T>() where T : Enumeration
{
foreach (var publicStatic in typeof(T).GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
Enumeration item = null;
item = (Enumeration)publicStatic.GetValue(null);
yield return item as T;
}
}
public static T With<T>(int index) where T : Enumeration
{
return GetAllFor<T>().SingleOrDefault(i => i.Index == index);
}
}
ValueObject simply covers off Equality functionality.
Elsewhere I use the static methods to get items from this enum (kinda like how you could use the core Enumeration static methods):
impact = Impact.With<Impact>(index.ImpactId.Value);
This is pretty handy but I want to know if I can get NHibernate to do this too when rehydrating objects.
Can it be done and how?
With an NHibernate Custom Type:
public class EnumerationType<T> : PrimitiveType where T : Enumeration
{
public EnumerationType()
: base(new SqlType(DbType.Int32))
{
}
public override object Get(IDataReader rs, int index)
{
object o = rs[index];
var value = Convert.ToInt32(o);
return Enumeration.With<T>(value);
}
public override object Get(IDataReader rs, string name)
{
int ordinal = rs.GetOrdinal(name);
return Get(rs, ordinal);
}
public override Type ReturnedClass
{
get { return typeof(T); }
}
public override object FromStringValue(string xml)
{
return int.Parse(xml);
}
public override string Name
{
get { return "Enumeration"; }
}
public override void Set(IDbCommand cmd, object value, int index)
{
var parameter = (IDataParameter)cmd.Parameters[index];
var val = (Enumeration)value;
parameter.Value = val.Value;
}
public override string ObjectToSQLString(object value, Dialect dialect)
{
return value.ToString();
}
public override Type PrimitiveClass
{
get { return typeof(int); }
}
public override object DefaultValue
{
get { return 0; }
}
}
If you're doing an HBM.xml-based mapping, you can set the custom type like this:
<property name="Impact" column="Impact" type="Namespace.To.EnumerationType`1[[Impact, AssemblyWithDomainEnum]], AssemblyWithNHibCustomType"/>
Alternatively, if you're using Fluent NHibernate, you can create a convention to map all enumeration types without having to configure each one individually:
public class EnumerationTypeConvention : IPropertyConvention, IPropertyConventionAcceptance
{
private static readonly Type _openType = typeof(EnumerationType<>);
public void Apply(IPropertyInstance instance)
{
var closedType = _openType.MakeGenericType(instance.Property.PropertyType);
instance.CustomType(closedType);
}
public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
{
criteria.Expect(x => typeof(Enumeration).IsAssignableFrom(x.Property.PropertyType));
}
}
And then add that convention however you like in your Fluent NHibernate configuration.
This seemed to work too, but perhaps Jimmy's way seems easier:
public class ImpactEnumType : IUserType
{
public SqlType[] SqlTypes
{
get
{
//We store our Impact in a single column in the database that can contain a int (for the index value)
SqlType[] types = new SqlType[1];
types[0] = new SqlType(DbType.Int32);
return types;
}
}
public Type ReturnedType
{
get { return typeof(Impact); }
}
public bool Equals(object x, object y)
{
// Impact is derived from ValueObject which implements Equals
return x.Equals(y);
}
public int GetHashCode(object x)
{
// as above
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
//We get the string from the database using the NullSafeGet used to get ints
int impactIndex = (int)NHibernateUtil.Int32.NullSafeGet(rs, names[0]);
// then pull the instance from the Enumeration type using the static helpers
return Impact.With<Impact>(impactIndex);
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
//Set the value using the NullSafeSet implementation for int from NHibernateUtil
if (value == null)
{
NHibernateUtil.Int32.NullSafeSet(cmd, null, index);
return;
}
value = (value as Impact).Index;
NHibernateUtil.Int32.NullSafeSet(cmd, value, index);
}
public object DeepCopy(object value)
{
//We deep copy the Impact by creating a new instance with the same contents
if (value == null) return null;
return Impact.With<Impact>((value as Impact).Index);
}
public bool IsMutable
{
get { return false; }
}
public object Replace(object original, object target, object owner)
{
//As our object is immutable we can just return the original
return original;
}
public object Assemble(object cached, object owner)
{
//Used for casching, as our object is immutable we can just return it as is
return cached;
}
public object Disassemble(object value)
{
//Used for casching, as our object is immutable we can just return it as is
return value;
}
}
My HBM XML:
<property name="Impact" column="ImpactIndex" type="namespace.childnamespace.ImpactEnumType, namespace.childnamespace" />
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.
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!
I'm dealing with a legacy database that has date and time fields as char(8) columns (formatted yyyyMMdd and HH:mm:ss, respectively) in some of the tables. How can i map the 2 char columns to a single .NET DateTime property? I have tried the following, but i get a "can't access setter" error of course because DateTime Date and TimeOfDay properties are read-only:
public class SweetPocoMannaFromHeaven
{
public virtual DateTime? FileCreationDateTime { get; set; }
}
.
mapping.Component<DateTime?>(x => x.FileCreationDateTime,
dt =>
{
dt.Map(x => x.Value.Date,
"file_creation_date");
dt.Map(x => x.Value.TimeOfDay,
"file_creation_time");
});
I have also tried defining a IUserType for DateTime, but i can't figure it out. I've done a ton of googling for an answer, but i can't figure it out still. What is my best option to handle this stupid legacy database convention? A code example would be helpful since there's not much out for documentation on some of these more obscure scenarios.
You need an ICompositeUserType to handle more than one column. You need to beef up the error checking, parsing formats, etc, but here is a starting point for you.
HTH,
Berryl
public class LegacyDateUserType : ICompositeUserType
{
public new bool Equals(object x, object y)
{
if (x == null || y == null) return false;
return ReferenceEquals(x, y) || x.Equals(y);
}
public int GetHashCode(object x) {
return x == null ? typeof (DateTime).GetHashCode() + 473 : x.GetHashCode();
}
public object NullSafeGet(IDataReader dr, string[] names, ISessionImplementor session, object owner)
{
if (dr == null) return null;
var datePortion = NHibernateUtil.String.NullSafeGet(dr, names[0], session, owner) as string;
var timePortion = NHibernateUtil.String.NullSafeGet(dr, names[1], session, owner) as string;
var date = DateTime.Parse(datePortion);
var time = DateTime.Parse(timePortion);
return date.AddTicks(time.Ticks);
}
///<summary>
/// Write an instance of the mapped class to a prepared statement. Implementors
/// should handle possibility of null values. A multi-column type should be written
/// to parameters starting from index.
///</summary>
public void NullSafeSet(IDbCommand cmd, object value, int index, ISessionImplementor session) {
if (value == null) {
// whatever
}
else {
var date = (DateTime) value;
var datePortion = date.ToString("your date format");
NHibernateUtil.String.NullSafeSet(cmd, datePortion, index, session);
var timePortion = date.ToString("your time format");
NHibernateUtil.String.NullSafeSet(cmd, timePortion, index + 1, session);
}
}
public object GetPropertyValue(object component, int property)
{
var date = (DateTime)component;
return property == 0 ? date.ToString("your date format") : date.ToString("your time format");
}
public void SetPropertyValue(object component, int property, object value)
{
throw new NotSupportedException("DateTime is an immutable object.");
}
public object DeepCopy(object value) { return value; }
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; }
///<summary>Get the "property names" that may be used in a query.</summary>
public string[] PropertyNames { get { return new[] { "DATE_PORTION", "TIME_PORTION" }; } }
///<summary>Get the corresponding "property types"</summary>
public IType[] PropertyTypes { get { return new IType[] { NHibernateUtil.String, NHibernateUtil.String }; } }
///<summary>The class returned by NullSafeGet().</summary>
public Type ReturnedClass { get { return typeof(DateTime); } }
///<summary>Are objects of this type mutable?</summary>
public bool IsMutable { get { return false; } }
}
=== fluent mapping (assuming automapping w/override classes) ====
public void Override(AutoMapping<MyClass> m)
{
....
m.Map(x => x.MyDateTime).CustomType<LegacyDateUserType>();
}