知道了many-to-many用法之后,many-to-one、one-to-many就很简单。因此参考这个系列前面几篇,可以很轻松的实现Vendor、Company、Plant、Item这几个对象。
Company与Vendor之间建立了一个一对多的关系,下面是Company类中Plants集合属性的定义
{
get { return _plant; }
set { _plant = value; }
}
private ISet<Plant> _plant;
下面是这个属性的映射配置
<key column="COMPANY_ID"/>
<one-to-many class="Plant" not-found="ignore" />
</set>
下面是Plant类中Company属性的定义与配置
{
get { return _company; }
set { _company = value; }
}
private Company _company;
2. 枚举类型
NHibernate直接支持枚举类型的映射,这种支持方式,在数据库中保存的是枚举的整数值。在TBLPLANTITEM中有三个字段对应的对象属性使用枚举类型:ITEM_CATEGORY、PURCHASE_CATEGORY、STOCK_OPTION,我把STOCK_OPTION字段设置成整数类型,用来测试NHB对枚举映射的直接支持方式,而其它两个设成了字符串类型,用于保存枚举的字符串描述。
在数据库保存枚举的字符串描述需要使用自定义映射类型,将在下面一节中讲述,本节看一下直接对枚举类型进行映射。
StockOptionEnum枚举的定义:
{
None=0,
ERP=1,
Hub=2
}
PlantItem类中StockOption属性的定义:
{
get { return _stockOption; }
set { _stockOption = value; }
}
private StockOptionEnum _stockOption;
属性的配置节点:
<column name="STOCK_OPTION" sql-type="int" not-null="false" />
</property>
NHibernate默认支持的枚举映射用起来很简单,这可能是能够将枚举值强制转化成整数这样一个值类型的原因。这种方式,枚举属性保存在数据库中的是整数值。
3. 自定义类型、自定义映射类型IUserType
首先在概念方面看一下。当你觉得某个属性需要满足的业务逻辑比较复杂,用.Net标准的数据类型无法满足你的需求时,你会选择为这个属性单独定义一个类,作为这个属性的类型,可以称这个为自定义类型/细粒度对象。某些情况下,将属性进行持久化映射时,并不是简单、直接的进行存取,也就是说你可能无法通过NHibernate提供的标准映射方法在属性和持久化媒介之间进行映射。这种情况下NHibernate提供一个机制,让你自己可以完全的控制映射行为,这就是为你的属性实现一个NHibernate.UserTypes.IUserType类,我称这个为自定义映射类型。自定义映射类型这个名称比较合适,因为从名称你就可以猜测到它的主要职责/作用就是完成属性与持久化媒介之间的映射。
我们先看一下怎样通过自定义映射类型保存枚举的字符串描述值。下面是ItemCategoryEnum、PurchaseCategoryEnum两个枚举的定义:
{
P=1, //Product
M=2, //material
} public enum PurchaseCategoryEnum
{
PO=1,
JIT=2
}
下面是自定义映射类型实现保存枚举的字符串描述。为了简化,我定义了一个抽象类实现NHibernate.UserTypes.IUserType,然后各个枚举的自定义映射类型就很容易实现了:
/// <summary>
/// 自定义枚举映射类型
/// </summary>
public abstract class MyEnumType : IUserType
{
private Type _type;
private int _length;
private MyEnumType()
{
}
public MyEnumType(Type type, int length)
{
this._type = type;
this._length = length;
}
/// <summary>
/// 自定义类型的对象实例是否会发生改变
/// </summary>
public bool IsMutable
{
get { return false; }
}
public Type ReturnedType
{
get { return _type; }
}
/// <summary>
/// 属性对应的数据库字段类型
/// </summary>
public SqlType[] SqlTypes
{
get { return new SqlType[] { new SqlType(DbType.String, this._length) }; }
}
/// <summary>
/// 如果IsMutable为true,此处应当实现对象实例的DeepCopy
/// </summary>
public object DeepCopy(object value)
{
return value;
}
public new bool Equals(object x, object y)
{
return x.ToString().Trim() == y.ToString().Trim();
}
public int GetHashCode(object x)
{
return x.ToString().GetHashCode();
}
/// <summary>
/// 从缓存中取对象
/// </summary>
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
/// <summary>
/// 将对象放入缓存前的处理
/// </summary>
public object Disassemble(object value)
{
return DeepCopy(value);
}
/// <summary>
/// 读取数据库字段值(DataReader),转换成实体属性
/// </summary>
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
object name = NHibernate.NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (name == null)
throw new Exception(_type.Name + " can not be null");
return Enum.Parse(_type, name.ToString(), true);
}
/// <summary>
/// 为属性的存取设置DbCommand参数
/// 这里我们要保存枚举的字符串描述,因此将value直接转换成字符串
/// </summary>
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if(value==null)
throw new Exception(_type.Name + " can not be null");
NHibernate.NHibernateUtil.String.NullSafeSet(cmd, value.ToString(), index);
}
public object Replace(object original, object target, object owner)
{
return original;
}
} public class PurchaseCategoryType : MyEnumType
{
public PurchaseCategoryType()
: base(typeof(PurchaseCategoryEnum), 5)
{
}
} public class ItemCategoryType : MyEnumType
{
public ItemCategoryType()
: base(typeof(ItemCategoryEnum), 3)
{
}
}
#endregion
下面是PlantItem类中PurchaseCategoryType和ItemCategoryType属性的定义:
{
get { return _itemCategory; }
set { _itemCategory = value; }
}
private ItemCategoryEnum _itemCategory; public virtual PurchaseCategoryEnum PurchaseCategory
{
get { return _purchaseCategory; }
set { _purchaseCategory = value; }
}
private PurchaseCategoryEnum _purchaseCategory;
这两个属性的映射配置:
<column name="ITEM_CATEGORY" sql-type="nvarchar" length="3" not-null="false" />
</property> <property name="PurchaseCategory" type="NH12.MyExample.Domain.PurchaseCategoryType, Domain">
<column name="PURCHASE_CATEGORY" sql-type="nvarchar" length="5" not-null="false" />
</property>
从上面可以看出,实体的属性使用枚举类型(或者其它的自定义类型),然后为枚举类型定义一个映射类型(上面的PurchaseCategoryType和ItemCategoryType),IUserType接口定义的主要职责,是如何将数据库取出来的值转换成枚举类型(或者自定义类型)(NullSafeGet方法)、如何将枚举类型(或者自定义类型)转换成数据库操作的DbParameter值(NullSafeSet方法),其它一些是支持NHibernate的OR机制必须的功能,例如了解属性类型(Type ReturnedType)、数据库字段的类型(SqlType[] SqlTypes)、对缓存机制的支持(Assemble、Disassemble方法)等等。
上面的示例演示如何通过自定义映射类型按照你的需要对属性进行持久化存取,实现NHibernate.UserTypes.IUserType也可以将一个属性保存到数据库的多个字段。下面演示将一个DateTime的属性按照yyyyMMdd、HHmmss这样的格式,以字符串的方式保存到数据库的两个字段中。
先用下面的语句为TBLPLANTITEM表添加两个字段:
CREATE_DATE nvarchar(8) NULL,
CREATE_TIME nvarchar(6) NULL
下面是自定义日期映射类型的实现:
{
public MyDateTimeType()
{
}
public bool IsMutable
{
get { return false; }
}
public Type ReturnedType
{
get { return typeof(DateTime); }
}
public SqlType[] SqlTypes
{
get
{
return new SqlType[] { new SqlType(DbType.String, 8), new SqlType(DbType.String, 6) };
}
}
public object DeepCopy(object value)
{
return value;
}
public new bool Equals(object x, object y)
{
return x == y;
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
object val1 = NHibernate.NHibernateUtil.String.NullSafeGet(rs, names[0]);
object val2 = NHibernate.NHibernateUtil.String.NullSafeGet(rs, names[1]);
string date = "1900-01-01", time = "00:00:00";
if (val1 != null && val1.ToString().Trim().Length == 8)
date = val1.ToString().Substring(0, 4) + "-" + val1.ToString().Substring(4, 2) + "-" + val1.ToString().Substring(6, 2);
if (val2 != null && val2.ToString().Length == 6)
time = val2.ToString().Substring(0, 2) + ":" + val2.ToString().Substring(2, 2) + ":" + val2.ToString().Substring(4, 2);
return DateTime.Parse(date + " " + time);
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
string date = "19000101", time = "000000";
if (value != null)
{
date = System.Convert.ToDateTime(value).ToString("yyyyMMdd");
time = System.Convert.ToDateTime(value).ToString("HHmmss");
}
NHibernate.NHibernateUtil.String.NullSafeSet(cmd, date, index);
NHibernate.NHibernateUtil.String.NullSafeSet(cmd, time, index + 1);
}
public object Replace(object original, object target, object owner)
{
return original;
}
}
下面是属性的定义:
{
get { return _createTime; }
set { _createTime = value; }
}
private DateTime _createTime;
下面是映射配置:
<column name="CREATE_DATE" sql-type="nvarchar(6)"/>
<column name="CREATE_TIME" sql-type="nvarchar(6)"/>
</property>
4. 组件类型component 组合主键composite-id
首先先理解OO里关联、聚合、组合三个概念。
在NHibernate中,one-to-one、many-to-many等这几种映射关系,基本上都用于实现聚合、组合这两种对象关系。他们使用上的形式是,定义A和B两个类,分别为A和B独立的编写配置文件完成映射配置,A跟B通常存储在不同的表中,通过使用字段关联实现这种关系。
NHibernate中的组件(component)/组合(composite),最通常的情况是实现某种形式的组合关系。在趋向于细粒度对象的设计中会有不少这样的情况,本来用几个属性表示也可以满足要求,但是因为存在一些特定的规则、业务等逻辑关系,希望将这几个属性用一个类做一次封装。举个用户名的例子,在数据库的用户表中,我使用FirstName、LastName两个字段保存用户名;在对象模型中,我定义一个UserName的类,包含FirstName、LastName、FullName等。我们关注两个方面,首先UserName和User对象是存储在同一个表中的,我们完全没有必要去建立一个one-to-one的映射关系;其次它跟前面自定义映射类型中的场景也是有区别的,自定义映射类型中的例子将数据库的一个或多个字段映射到一个属性上,而这里UserName的例子,需要将数据库的多个字段映射到多个属性。从实现层面来看,UserName完全是一个独立的类,需要完成自己的映射;从概念层次看,它跟User对象是组合关系,数据保存在共同的地方。
我举的例子很简单,但是能够看明白组件类型的使用。TBLPLANTITEM表用两个字段PLANT_ID、ITEM_ID作为主键,我们可以定义一个组件类PlantItemID,作为PlantItem对象的ID属性:
{
private string _plantID;
private string _itemID;
public PlantItemID()
{
}
public PlantItemID(string plantID, string itemID)
{
_plantID = plantID;
_itemID = itemID;
}
public virtual string PlantID
{
get { return _plantID; }
set { _plantID = value; }
}
public virtual string ItemID
{
get { return _itemID; }
set { _itemID = value; }
}
#region override
public override bool Equals(object obj)
{
PlantItemID o = obj as PlantItemID;
if (o == null)
return false;
return this.PlantID == o.PlantID && this.ItemID == o.ItemID;
}
public override int GetHashCode()
{
return this.PlantID.GetHashCode() + this.ItemID.GetHashCode();
}
public override string ToString()
{
return this.PlantID + " " + this.ItemID;
}
#endregion
}
属性和映射配置文件如下:
{
get { return _id; }
set { _id = value; }
}
private PlantItemID _id;
<key-property column="PLANT_ID" name="PlantID" type="String" length="5" />
<key-property column="ITEM_ID" name="ItemID" type="String" length="5" />
</composite-id>
这是很简单的例子,我用一个组件类型实现组合主键(composite-id),对于组件类型仅用于普通属性的时候,使用component映射元素,在component元素下面除了property之外,也可以使用many-to-one、set等元素,因此可以构造很丰富的组建类型出来。对于普通的组件类型,不需要重载Equals、GetHashCode这两个方法,但对于组合主键一定要重写,这是因为NHibernate要使用这些方法判断两个对象的主键是否一样,确定两个对象是否相等。
到目前我们已经可以写出完整的Company、Plant、Item、PlantItem这几个类和相关的映射配置文件了,下面看一下如何使用:
ISession session = null;
ITransaction tran = null;
try
{
session = sessionFactory.OpenSession();
tran = session.BeginTransaction();
Company company
= new Company("1000", "test company 1", "", new HashedSet<Plant>());session.Save(company);
Plant plant1 = new Plant("1101", "test plant 1", company);
session.Save(plant1);
Plant plant2 = new Plant("1102", "test plant 2", company);
session.Save(plant2);
Item item1 = new Item("FK1.1023.78AF", "2.5# LCD"