NHibernate对象关联映射
作者:陈省
介绍
在面向对象开发中,运行时的数据的关联是通过对象之间的引用来实现的。而静态数据的关联是通过表间的外键关联来实现的。而对于一个ORMapping的框架来说,管理对象之间的关联和表间的关系是框架的核心部分。最常见的对象之间的关联是1对多的关联,比如父/子关联,这类关联的数据存储主要是采用主从表的方式来实现,还有一种关联的是多对多的关联,这种关联的数据存储需要将一个n:m关联分解为两个1:n的关联来实现。
对象关联映射实例
接下来,我们将通过一个实例分析来看看NHibernate如何来实现对象关联。假设我们现在要开发一个客户关系管理系统,系统中有三类对象,客户类(Customer)、销售类(Sale)以及订单类(Order),它们之间的关系是客户类同订单类之间的关系是1对多的聚集关系,一个客户可能会下多份订单,订单不能脱离客户而独立存在。而客户类同销售类之间的关系是多对多,根据分析建立的类图示意如下:

其中,Customer类具有CustId属性用于唯一标识Customer对象,CustName对应于客户名称。Order类的OrderId是订单的唯一标识,Content属性描述订单的内容,Amount属性对应于订单的金额,最后一个Index属性描述订单的先后顺序。Sale类同Customer类似,也是有名称和ID属性。
类定义的实现
首先根据设计创建各个类的定义,首先是客户类的定义:
public class Customer
{
public Customer()
{
//
// TODO: 在此处添加构造函数逻辑
//
}
public override string ToString()
{
return this.CustName;
}
private int _CustId;
//顾客ID
public int CustId
{
get { return _CustId; }
set { _CustId = value; }
}
private string _CustName;
//顾客名称
public string CustName
{
get { return _CustName; }
set { _CustName = value; }
}
private IDictionary _Sales;
public System.Collections.IDictionary Sales
{
get {
return _Sales;
}
set { _Sales = value; }
}
private IList _Orders;
public IList Orders
{
get
{
if (_Orders==null)
_Orders=new ArrayList();
return _Orders;
}
set
{
_Orders=value;
}
}
}
注意在客户类的定义中除了基本属性外,我们还定义了类型为IDictionary的Sales属性,建立了客户类同销售类的1对n的单向关联,注意这里用IDictionary类型是因为客户同销售人员的关联中,顺序是无意义。而客户同订单之间的关联采用IList类型定义的Orders属性来实现,是因为IList具有索引属性,可以表示订单的先后顺序。
下面是订单类的定义:
public class Order
{
public Order()
{
//
// TODO: 在此处添加构造函数逻辑
//
}
private int _OrderId;
//订单ID
public int OrderId
{
get { return _OrderId; }
set { _OrderId = value; }
}
private double _Amount;
//订单金额
public double Amount
{
get { return _Amount; }
set { _Amount = value; }
}
private string _Content;
//订单内容
public string Content
{
get { return _Content; }
set { _Content = value; }
}
private Customer _Cust;
public NHOrder.Customer Cust
{
get { return _Cust; }
set { _Cust = value; }
}
public int Index
{
get {return this.Cust.Orders.IndexOf(this);}
set {}
}
}
要说明的是订单类通过一个Customer类型的Cust属性建立了同客户类的1:1的关联,结合Customer同Order类关联,形成了客户类同订单类的双向关联。另外,Index属性同其它属性不同,属性值只能是Order类在Customer类的Orders列表中的索引(否则NHibernate会无法加载),因此Set方法无须实现。
下面是销售人员类的定义:
public class Sale
{
public Sale()
{
//
// TODO: 在此处添加构造函数逻辑
//
}
public override string ToString()
{
return this.SaleName;
}
private int _SaleId;
//销售ID
public int SaleId
{
get { return _SaleId; }
set { _SaleId = value; }
}
private string _SaleName;
//销售名称
public string SaleName
{
get { return _SaleName; }
set { _SaleName = value; }
}
private IDictionary _Customers;
public System.Collections.IDictionary Customers
{
get {
if (_Customers==null)
_Customers=new Hashtable();
return _Customers;
}
set { _Customers = value; }
}
}
销售人员类同客户类类似,也通过IDictionary类型的Cutomers集合属性实现了同客户类之间的多对多的关联。
映射关系定义
有了类定义,接下来需要定义类同数据库之间的关联映射,先来看客户类的定义:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class name="NHOrder.Customer, NHOrder" table="customers">
<id name="CustId" column="CustId" type="Int32">
<generator class="identity" />
</id>
<property name="CustName" type="String" length="40"/>
<set name="Sales" table="customersale" cascade="save-update" inverse="false" lazy="false">
<key column="CustId" />
<many-to-many column="SaleId" class="NHOrder.Sale, NHOrder" />
</set>
<list name="Orders" cascade="all" table="orders">
<key column="CustId" />
<index column="OrderIndex" type="Int32"/>
<one-to-many class="NHOrder.Order, NHOrder" />
</list>
</class>
</hibernate-mapping>
定义中的list元素表示客户类同订单类的关联映射为IList类型的属性,元素的Index自元素指定了订单表orders的OrderIndex字段对应于订单的顺序,one-to-many元素则指明同Customer类具有一对多关系的类是NHOrder.Order类,还要注意的是list元素的cascade属性值为all,这表示Customer支持级联更新和删除操作,也就是说当用户删除或修改了客户对象时,客户对象对应的所有订单类也被删除或修改,通过这种客户类同订单类的聚集关系。
Xml文件定义中的Set元素标识了客户类同销售类之间的关联是用集合类IDictionary来定义的,many-to-many元素标识了两者多对多的关联。注意Set元素的cascade属性为save-update表示关联只支持级联更新,而不支持级联删除,因为客户同销售人员不是聚集关系,删除一个客户把对应的销售人员也都删除显然是不合理的。Inverse属性则表示两者的关联的反转控制的,也就是说关联关系的保存是由销售类来控制的。Laze属性置为false,表示客户类加载后,同时会将所有关联的销售类也加载进内存,如果为true,则只有当用户真正访问Customer类的Sales属性时才加载相应的数据。
相对于客户类的映射定义,订单类和销售类的映射定义相对要简单一些,订单映射内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class name="NHOrder.Order, NHOrder" table="orders">
<id name="OrderId" column="OrderId" type="Int32">
<generator class="identity" />
</id>
<property name="Amount" type="Double" />
<property name="Content" type="String" length="200"/>
<many-to-one name="Cust" column="CustId" class="NHOrder.Customer, NHOrder"/>
<property name="Index" type="Int32" update="true" insert="true" column="OrderIndex"/>
</class>
</hibernate-mapping>
销售映射定义如下:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class name="NHOrder.Sale, NHOrder" table="sales">
<id name="SaleId" column="SaleId" type="Int32">
<generator class="identity" />
</id>
<property name="SaleName" type="String" length="40"/>
<set name="Customers" table="customersale" cascade="save-update" inverse="true" lazy="false">
<key column="SaleId" />
<many-to-many column="CustId" class="NHOrder.Customer, NHOrder" />
</set>
</class>
</hibernate-mapping>
定义中的订单定义中的many-to-one表示订单类同客户类的关联是多对1的关联。而销售定义中的many-to-many元素定义了销售同客户类的双向关联。
表示层定义
业务域模型定义好之后,需要设计表示层展现方式,下图是界面设计:

界面中有两个列表,分别对应于客户、订单和销售列表,其中订单列表的展示使用DataGrid来实现,是因为订单类的属性比较多,客户类只需要展示客户名称属性,因此用列表框来展示,销售人员列表则使用CheckListBox来展现,当列表中的项目被打勾时,表示客户同该销售人员有关联关系。三个列表之间具有联动关系,选中某个客户时,订单列表中只显示相应订单,销售列表中只有同该客户关联的销售项才打勾。
此外,界面上定义了添加和删除客户的按钮,以及添加客户的订单的按钮。界面上方有两个按钮,一个是数据库Schema生成按钮,一个是查询数据库中数据的按钮,点击后会加载所有的对象数据。
应用逻辑实现
数据库初始化
数据库初始化包括两部分,一个是生成数据库的DbSchema,一个是初始化销售人员列表,前面我们没有根据映射文件来定义数据库生成脚本,这是因为NHibernate提供了SchemaExport类,它可以根据系统内定义的hbm.xml文件,自动生成建库和销毁数据库的代码(注意:由于NHibernate还没有移植Hibernate的所有功能,所以暂时还不能生成数据库迭代演化的修改脚本,因此使用时会破坏原有的数据库数据)。代码示意如下:
private void btnInit_Click(object sender, System.EventArgs e)
{
//初始化数据库,注意,这个方法先Drop,然后Create。会破坏
new SchemaExport(cfg).Create(true, true);
//创建事先定义好的销售列表
ISession session=factory.OpenSession();
Sale s=new Sale();
s.SaleName="销售甲";
chklstSale.Items.Add(s);
session.Save(s);
s=new Sale();
s.SaleName="销售乙";
chklstSale.Items.Add(s);
session.Save(s);
session.Flush();
session.Close();
}
ISessionFactory factory;
Configuration cfg;
private void OrderForm_Load(object sender, System.EventArgs e)
{
try
{
cfg = new Configuration();
cfg.AddAssembly("NHOrder");
factory = cfg.BuildSessionFactory();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
要说明的SchemaExport是通过Configuration类获取作为资源嵌入到可执行文件中的hbm.xml文件内容的,资源的加载是通过Configuration的AddAssembly方法实现的。
数据查询
数据查询会将所有的对象数据加载到内存,代码如下:
private void btnQuery_Click(object sender, System.EventArgs e)
{
this.lbCust.Items.Clear();
this.chklstSale.Items.Clear();
//获取当前的客户信息
ISession session=factory.OpenSession();
//获取当前的销售信息
IList saleLst=session.CreateCriteria(typeof(Sale)).List();
foreach (Sale s in saleLst)
this.chklstSale.Items.Add(s);
IList custLst = session.CreateCriteria(typeof(Customer)).List();
session.Close();
foreach (Customer cust in custLst)
this.lbCust.Items.Add(cust);
if (lbCust.Items.Count>0)
this.lbCust.SelectedIndex=0;
}
在代码的最后,会设定客户列表的当前的SelectedIndex属性设置为0,这会激发客户列表框的SelectedIndexChanged事件,联动的更新订单列表和销售列表的状态,代码如下:
private Customer GetCurrentCustomer()
{
return (Customer)this.lbCust.Items[lbCust.SelectedIndex];
}
private bool AutoUpdate;
private void lbCust_SelectedIndexChanged(object sender, System.EventArgs e)
{
if (this.lbCust.SelectedIndex==-1)
return;
Customer cust=this.GetCurrentCustomer();
this.dgOrders.DataSource=null;
this.dgOrders.DataSource=cust.Orders;
//将客户同销售引用关联
AutoUpdate=true;
try
{
for (int i=0;i<this.chklstSale.Items.Count;i++)
this.chklstSale.SetItemChecked(i,false);
if (cust.Sales==null)
return;
foreach (Sale s in cust.Sales.Values)
{
this.chklstSale.SetItemChecked(this.chklstSale.Items.IndexOf(s),true);
}
}
finally
{
AutoUpdate=false;
}
}
因为DataGrid的数据绑定支持IList,因此订单数据同DataGrid的绑定直接设定DataSource属性就可以了。
添加客户
对于一对多的关联关系来说,添加完订单后一定要将Order对象添加到客户类的列表中,同时要设定订单对象的Cust属性为当前的客户对象,否则无法正确保存订单对象及其同客户类的关联,加载Customer.Orders列表对象时NHibernate也就无法根据外键自动地加载数据了。
private void btnAddOrder_Click(object sender, System.EventArgs e)
{
NewOrderForm dlg=new NewOrderForm();
if (dlg.ShowDialog()!=DialogResult.OK)
return;
ISession session = factory.OpenSession();
Order order=new Order();
order.Amount=dlg.Amount;
order.Content=dlg.Content;
Customer cust=this.GetCurrentCustomer();
order.Cust=cust;
cust.Orders.Insert(cust.Orders.Count, order);
session.Save(order);
session.Flush();
session.Close();
this.lbCust_SelectedIndexChanged(null,null);
}
还要说明的一点就是,session.save方法并不会将数据马上写入到数据库中,而是当session.flush方法时才会将数据真正写入到数据库中,这样可以实现批量数据的修改,提高性能。
删除客户
客户数据的删除比较简单,只要调用session.delete方法就可以了,代码如下:
private void btnDelCust_Click(object sender, System.EventArgs e)
{
ISession session=factory.OpenSession();
ITransaction transaction=session.BeginTransaction();
try
{
Customer cust=this.GetCurrentCustomer();
session.Delete(cust);
transaction.Commit();
this.dgOrders.DataSource=null;
AutoUpdate=true;
for (int i=0;i<this.chklstSale.Items.Count;i++)
this.chklstSale.SetItemChecked(i,false);
AutoUpdate=false;
this.lbCust.Items.RemoveAt(lbCust.SelectedIndex);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
if (!transaction.WasCommitted)
transaction.Rollback();
}
finally
{
session.Close();
}
}
运行程序后,输入一些数据,点击删除客户按钮后,你会发现数据库中的对应该客户的订单数据也都被删除,实现了级联删除,同时同该客户关联的销售人员则没有被删除。
关联客户-销售人员
当用户改变销售人员列表中的打勾的状态时,会激发列表框的ItemChecked事件,我们可以在该事件中实现关联功能。同一对多关联的保存不同,对于多对多的关联来说,每增加或删除一个关联,关联两端的对象都需要修改各自的集合属性,同时两端都要进行保存,代码示意如下:
private void chklstSale_ItemCheck(object sender, System.Windows.Forms.ItemCheckEventArgs e)
{
if (AutoUpdate)
return;
//将客户同销售进行引用关联
ISession session=factory.OpenSession();
Customer cust=this.GetCurrentCustomer();
Sale s=(Sale)this.chklstSale.Items[e.Index];
if (e.NewValue==CheckState.Checked)
{
//添加关联
cust.Sales.Add(s,s);
s.Customers.Add(cust,cust);
session.SaveOrUpdate(cust);
session.SaveOrUpdate(s);
session.Flush();
}
else if (e.NewValue==CheckState.Unchecked)
{
cust.Sales.Remove(s);
s.Customers.Remove(cust);
session.SaveOrUpdate(cust);
session.SaveOrUpdate(s);
session.Flush();
}
session.Close();
}
总结
在本文中,由于篇幅的限制,我们只实现了对象之间最常见的一对多,多对多的关联,事实上NHibernate功能远远不止这些,NHibernate还支持一对一关联,继承关联,组件关联等等,有兴趣的朋友可以自行通过实例和相关文档研究。
|