假设有这样一个枚举:
/// <summary> /// 字典项类型 /// </summary> public enum DicItemType { [EnumDescription("程序使用")] Program = 0, [EnumDescription("用户自定义")] Custom = 1 }
NHibernate默认是映射为数据库中的数字类型,也就是0或者1。当我们使用数据库管理工具(例如PLSql/Developer)直接浏览数据库里的数据时,看到的一排又一排的0或者1,可读性不太好,总是要再去找枚举定义看0代表的是什么,1代表的又是什么。当使用存储过程写一些复杂查询时,也会写 CASE WHEN ITEM_TYPE = 1 THEN ... 或者 WHERE ITEM_TYPE = 1 这样包含了神奇数字的语句,可读性同样不好。虽然可以在后面跟上注释,也还是很烦。能不能让NHibernate把枚举保存为数据库里的字符串呢?例如不是保存0或者1,而是保存为 "Program" 或者 "Custom"。答案是肯定的。NHibernate提供了自定义类型的机制,通过将枚举映射为一个自定义类型,就可以自定义从数据库中读取枚举数据和保存枚举到数据库中的操作。下面详细描述一下实现方法。环境:.Net Framework 4.0,Oracle 11g R2,NHibernate 3.3.1.4000,FluentNHibernate 1.3.0.733,使用Oracle的64位ODP Oracle.DataAccess 4.112.3.0。
新增自定义类型
首先,需要新增一个自定义类型 NullableEnumCusType,它实现 IUserType 接口。这个自定义类型告诉NHibernate要把枚举保存为数据库中的 NHibernateUtil.AnsiString.SqlType 类型,以及如何从数据库中读取和保存枚举值(经由 NullSafeGet() 和 NullSafeSet() 方法)。
using System; using NHibernate; using NHibernate.SqlTypes; using NHibernate.UserTypes; namespace Zen.Framework.Data { public class EnumCusType<TEnum> : IUserType where TEnum:struct { 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 bool Equals(object x, object y) { return x.Equals(y); // 由于装箱后 x == y 会总是返回 false, 所以要使用 Equals() } public int GetHashCode(object x) { return x.GetHashCode(); } public bool IsMutable { get { return true; } } public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner) { return Enum.Parse(typeof(TEnum), NHibernate.NHibernateUtil.AnsiString.NullSafeGet(rs, names[0]).ToString()); } public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index) { NHibernate.NHibernateUtil.AnsiString.NullSafeSet(cmd, value.ToString(), index); } public object Replace(object original, object target, object owner) { return target; } public Type ReturnedType { get { return typeof(TEnum); } } public NHibernate.SqlTypes.SqlType[] SqlTypes { get { return new SqlType[] { NHibernateUtil.AnsiString.SqlType }; } } } }
使用自定义类型
在实体的枚举属性映射时,要指定上面新增的自定义枚举类型。这样就已经可以实现枚举与数据库字符串之间的映射和转换了。
Map(t => t.ItemType, "ITEM_TYPE").CustomType<EnumCusType<DicItemType>>();
配置可为空类型的枚举
对于可为空类型的属性,例如:
/// <summary> /// 字典项类型2 /// </summary> public virtual DicItemType? ItemType2 { get; set; }
需要在增加一个针对可为空枚举的自定义类型:
using System; using NHibernate; using NHibernate.SqlTypes; using NHibernate.UserTypes; namespace Zen.Framework.Data { public class NullableEnumCusType<TEnum> : 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 bool Equals(object x, object y) { if (x == null) { return x == y; } else { return x.Equals(y); // 由于装箱后 x == y 会总是返回 false, 所以要使用 Equals() } } public int GetHashCode(object x) { return x.GetHashCode(); } public bool IsMutable { get { return true; } } public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner) { var v = NHibernate.NHibernateUtil.AnsiString.NullSafeGet(rs, names[0]); if (v == null) { return null; } else { return Enum.Parse(typeof(TEnum).GetGenericArguments()[0], v.ToString()); } } public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index) { NHibernate.NHibernateUtil.AnsiString.NullSafeSet(cmd, (value == null? null : value.ToString()), index); } public object Replace(object original, object target, object owner) { return target; } public Type ReturnedType { get { return typeof(TEnum); } } public NHibernate.SqlTypes.SqlType[] SqlTypes { get { return new SqlType[] { NHibernateUtil.AnsiString.SqlType }; } } } }
然后把可为空类型的实体属性映射为可为空的自定义枚举类型,这样可为空的枚举也没问题了。
Map(t => t.ItemType2, "ITEM_TYPE2").CustomType<NullableEnumCusType<DicItemType>>();
使用 EnumConvention 统一配置自定义枚举类型
之前,我们是可以在 EnumConvention 中对枚举类型进行统一配置的,也就是说,在实体映射的时候,不需要区分枚举和普通的字符型以及数字型属性,例如可以这样:
/// <summary> /// 字典类别 /// </summary> public class DicItemEntityMap : InputItemMap<DicItem> { public DicItemEntityMap() { Table("SYS_DIC_ITEM"); Id(t => t.Id, "DIC_ITEM_ID"); Map(t => t.IsStop, "IS_STOP"); Map(t => t.ItemType, "ITEM_TYPE"); Map(t => t.ItemType2, "ITEM_TYPE2"); References(t => t.Category, "DIC_CATEGORY_ID"); } }
这是因为 FluentNHibernate 提供了 Convention 机制可以对各种类型进行统一配置。例如只要实现这样一个 EnumConvention 就可以对所有枚举统一配置:
public class EnumConvention : IUserTypeConvention { public void Accept(FluentNHibernate.Conventions.AcceptanceCriteria.IAcceptanceCriteria<FluentNHibernate.Conventions.Inspections.IPropertyInspector> criteria) { // 匹配枚举或可为空枚举 criteria.Expect(x => x.Property.PropertyType.IsEnum || (x.Property.PropertyType.Name == "Nullable`1" && x.Property.PropertyType.GetGenericArguments().Length > 0 && x.Property.PropertyType.GetGenericArguments()[0].IsEnum)); } public void Apply(FluentNHibernate.Conventions.Instances.IPropertyInstance instance) { instance.CustomType(instance.Property.PropertyType); } }
能不能把上面的 EnumConvention 改造一下,适配新增的 EnumCusType 和 NullableEnumCusType 呢?这样就不用每次都写 ” Map(t => t.ItemType, "ITEM_TYPE").CustomType<EnumCusType<DicItemType>>() “ 这样烦人的配置了。
但是你马上就会发现 EnumCusType 是非常坑爹的泛型,在 EnumConvention 里没法直接调用 instance.CustomType() 进行配置。好在微软提供了非常强大的Emit,动态生成一个泛型类型非常轻松:
public class EnumConvention : IUserTypeConvention { public void Accept(FluentNHibernate.Conventions.AcceptanceCriteria.IAcceptanceCriteria<FluentNHibernate.Conventions.Inspections.IPropertyInspector> criteria) { // 匹配枚举或可为空枚举 criteria.Expect(x => x.Property.PropertyType.IsEnum || (x.Property.PropertyType.Name == "Nullable`1" && x.Property.PropertyType.GetGenericArguments().Length > 0 && x.Property.PropertyType.GetGenericArguments()[0].IsEnum)); } public void Apply(FluentNHibernate.Conventions.Instances.IPropertyInstance instance) { //instance.CustomType(instance.Property.PropertyType); if (instance.Property.PropertyType.Name == "Nullable`1") { // 转换为可空枚举自定义类型 Type t = TypeBuilder.GetType( string.Format("Zen.Framework.Data.NullableEnumCusType`1[[{0}]]", instance.Property.PropertyType.AssemblyQualifiedName)); instance.CustomType(t); } else { // 转换为非可空枚举自定义类型 Type t = TypeBuilder.GetType( string.Format("Zen.Framework.Data.EnumCusType`1[[{0}]]", instance.Property.PropertyType.AssemblyQualifiedName)); instance.CustomType(t); } } }