目录
写在前面
在前面的文章中,我们只使用了一个Customer类进行举例,而在客户、订单、产品中它们的关系,咱们并没有涉及,比如一个客户可以有一个或者多个订单,在数据库中变现为“主外键关系”,有时也喜欢称为“父子关系”。那么就让我们一起学习,在nhibernate中,是如何处理这种关系的吧?
文档与系列文章
一对多关系
首先看一下数据表的关系。
这里先使用Customer和Order的关系为例进行说明,一个客户可以有一个或者多个订单的关系。
在nhibernate中定义了多种集合方式:
Bag:对象集合,集合中的元素可以重复。例如:{1,2,3,4,2,3},相当于.Net中的IList和IList<T>。
Set:对象集合,集合中的元素必须唯一。例如:{1,3,4},相当于.Net中的ISet和ISet<T>,nhibernate的Iesi.Collections.dll程序集提供ISet集合。
List:整数索引集合,集合中的元素可以重复。例如:{
{1,"zhangsan"},{2,"lisi"}},相当于.Net中ArrayList和List集合。Map:键值对集合,相当于.Net中的IDictionary<TKey,TValue>和Hashtable。
使用过Entity Framework的朋友都知道,在EF中使用的ISet,为了保持一直,这里也使用ISet集合,也方便以后如果使用ef的时候,用起来更顺手。
一个例子
在nhibernate-4.0版本中,在命名空间Iesi.Collections.Generic中已经没有ISet集合了,那么我们使用.net中的ISet代替来描述这种一对多的关系,Customer.cs类代码如下:
1 ///2 /// 描述:客户实体,数据库持久化类 3 /// 创建人:wolfy 4 /// 创建时间:2014-10-16 5 /// 6 public class Customer 7 { 8 ///9 /// 客户id10 /// 11 public virtual Guid CustomerID { get; set; }12 ///13 /// 客户名字14 /// 15 public virtual Name NameAddress { get; set; }16 ///17 /// 版本控制18 /// 19 public virtual int Version { get; set; }20 ///21 /// 一对多关系:一个Customer有一个或者多个Order22 /// 23 public virtual System.Collections.Generic.ISetOrders { set; get; }24 }
修改Order类
1 ///2 /// 描述:订单实体,数据库持久化类 3 /// 创建人:wolfy 4 /// 创建时间:2014-10-16 5 /// 6 public class Order 7 { 8 ///9 /// 订单id10 /// 11 public virtual Guid OrderID { set; get; }12 ///13 /// 下订单时间14 /// 15 public virtual DateTime OrderDate { set; get; }16 ///17 /// 下订单的客户,多对一的关系:orders对应一个客户18 /// 19 public virtual Customer Customer { set; get; }20 }
在nhibernate中,可以通过映射文件来关联对象之间的关系。
- 对象之间的关系:一对一,多对一,一对多,多对多关系。
- 在关系中控制级联行为(Cascade behavior):级联删除,级联更新。
- 父子关系的双向导(bidirectional navigation)
父实体映射
父实体Customer映射文件中,定义了:
集合类型:Set,Bag,List,Map
在保存,更新,删除操作时的级联行为。
关联的控制方向:
inverse=“false”(默认)父实体负责维护关联关系。
inverse=“true”子实体负责维护关联关系。
与子实体的关系:一对多,多对一,多对多。
修改Customer.hbm.xml映射文件,描述一个客户对应多个订单的关系
1 2 34 5 6 277 10 118 9 12 13 14 15 20 21 2217 19 23 2624 25
添加Order.hbm.xml映射文件
1 23 4 145 76 9 10 13
many-to-one节点属性介绍:
子实体(Order)映射定义:与父实体关联的(多对一、一对多、多对多) 关系,并用一个指针来导航到父实体。
在“子”端通过many-to-one元素定义与“父”端的关联,从“子”端角度看这种关系模型是多对一关联(实际上是对Customer对象的引用)。下面看看many-to-one元素映射属性:
access(默认property):可选field、property、nosetter、ClassName值。NHibernate访问属性的策略。
cascade(可选):指明哪些操作会从父对象级联到关联的对象。可选all、save-update、delete、none值。除none之外其它将使指定的操作延伸到关联的(子)对象。
class(默认通过反射得到属性类型):关联类的名字。
column(默认属性名):列名。
fetch(默认select):可选select和join值,select:用单独的查询抓取关联;join:总是用外连接抓取关联。
foreign-key:外键名称,使用SchemaExport工具生成的名称。
index:......
update,insert(默认true):指定对应的字段是否包含在用于UPDATE或INSERT 的SQL语句中。如果二者都是false,则这是一个纯粹的 “外源性(derived)”关联,它的值是通过映射到同一个(或多个)字段的某些其他特性得到或者通过触发器其他程序得到。
lazy:可选false和proxy值。是否延迟,不延迟还是使用代理延迟。
name:属性名称propertyName。
not-found:可选ignore和exception值。找不到忽略或者抛出异常。
not-null:可选true和false值。
outer-join:可选auto、true、false值。
property-ref(可选):指定关联类的一个属性名称,这个属性会和外键相对应。如果没有指定,会使用对方关联类的主键。这个属性通常在遗留的数据库系统使用,可能有外键指向对方关联表的某个非主键字段(但是应该是一个唯一关键字)的情况下,是非常不好的关系模型。比如说,假设Customer类有唯一的CustomerId,它并不是主键。这一点在NHibernate源码中有了充分的体验。
unique:可选true和false值。控制NHibernate通过SchemaExport工具生成DDL的过程。
unique-key(可选):使用DDL为外键字段生成一个唯一约束。
编写OrderData.cs代码,添加Order。
1 ///2 /// 描述:订单数据层 3 /// 创建人:wolfy 4 /// 创建时间:2014-11-02 5 /// 6 public class OrderData 7 { 8 ///9 /// 添加订单10 /// 11 /// 12 ///13 public bool AddOrder(Order order)14 {15 try16 {17 NHibernateHelper nhibernateHelper = new NHibernateHelper();18 var session = nhibernateHelper.GetSession();19 session.SaveOrUpdate(order);20 session.Flush();21 return true;22 }23 catch (Exception)24 {25 throw;26 }27 }28 }
测试页面AddCustomer.aspx
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
1 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="CustomerManager.aspx.cs" Inherits="Wolfy.Shop.WebSite.CustomerManager" %> 2 3 4 5 6 7 873 749 32 33 34
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.UI; 6 using System.Web.UI.WebControls; 7 using Wolfy.Shop.Domain.Entities; 8 9 namespace Wolfy.Shop.WebSite 10 { 11 public partial class CustomerManager : System.Web.UI.Page 12 { 13 protected void Page_Load(object sender, EventArgs e) 14 { 15 if (!IsPostBack) 16 { 17 RepeaterDataBind(); 18 //Listlst = new Business.CustomerBusiness().GetAllCustomerID() as List ; 19 //foreach (var item in lst) 20 //{ 21 // Response.Write(item.ToString()); 22 //} 23 // Response.Write(new Business.CustomerBusiness().GetCustomerByName("wolfy").FirstOrDefault().CustomerName); 24 } 25 } 26 private void RepeaterDataBind() 27 { 28 Business.CustomerBusiness customerBusiness = new Business.CustomerBusiness(); 29 this.rptCustomerList.DataSource = customerBusiness.GetCustomerList(c => 1 == 1); //customerBusiness.GetCustomers(); 30 this.rptCustomerList.DataBind(); 31 } 32 /// 33 /// 添加客户信息 34 /// 35 /// 36 /// 37 protected void btnAdd_Click(object sender, EventArgs e) 38 { 39 Guid guidCustomerID = Guid.NewGuid(); 40 var customer = new Customer() { NameAddress = new Name() { CustomerName = "zhangsan3322", CustomerAddress = "北京 海淀" }, CustomerID = guidCustomerID }; 41 Business.CustomerBusiness customerBusiness = new Business.CustomerBusiness(); 42 //使用事务的方式添加客户信息 43 if (customerBusiness.SaveOrUpdateByTrans(customer)) 44 { 45 RepeaterDataBind(); 46 } 47 //提供一个名字长度溢出的测试数据 48 customer = new Customer() { NameAddress = new Name() { CustomerName = "zhangsan3322", CustomerAddress = "北京 海淀" }, CustomerID = Guid.NewGuid() }; 49 //使用事务的方式添加客户信息 50 if (customerBusiness.SaveOrUpdateByTrans(customer)) 51 { 52 RepeaterDataBind(); 53 } 54 } 55 ///56 /// 查询 57 /// 58 /// 59 /// 60 protected void btnSearch_Click(object sender, EventArgs e) 61 { 62 63 this.rptCustomerList.DataSource = new Business.CustomerBusiness().GetCustomerAddress(this.txtName.Text, this.txtAddress.Text); 64 this.rptCustomerList.DataBind(); 65 } 66 ///67 /// 并发更新操作 68 /// 69 /// 70 /// 71 protected void btnSameTimeUpdate_Click(object sender, EventArgs e) 72 { 73 Guid guidCustomerId = new Guid("82724514-682E-4E6F-B759-02E499CDA50F"); 74 //模拟第一个修改数据 75 Customer c1 = new Customer() 76 { 77 CustomerID = guidCustomerId, 78 NameAddress = new Name() { CustomerName = "zhangsan3322", CustomerAddress = "北京 海淀" }, 79 Version = 1 80 }; 81 //模拟第二个修改数据 82 Customer c2 = new Customer() 83 { 84 CustomerID = guidCustomerId, 85 NameAddress = new Name() { CustomerName = "zhangsan3322", CustomerAddress = "北京 海淀" }, 86 Version = 1 87 }; 88 89 Business.CustomerBusiness customerBusiness = new Business.CustomerBusiness(); 90 customerBusiness.SaveOrUpdateByTrans(c1); 91 customerBusiness.SaveOrUpdateByTrans(c2); 92 } 93 ///94 /// 下订单 95 /// 96 /// 97 /// 98 protected void lnkOrder_Click(object sender, EventArgs e) 99 {100 LinkButton lnkBtn = sender as LinkButton;101 if (lnkBtn!=null)102 {103 Response.Redirect("AddOrder.aspx?cid="+lnkBtn.CommandArgument);104 }105 }106 }107 }
AddOrder.aspx.cs
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.UI; 6 using System.Web.UI.WebControls; 7 using Wolfy.Shop.Domain.Entities; 8 9 namespace Wolfy.Shop.WebSite10 {11 public partial class AddOrder : System.Web.UI.Page12 {13 protected void Page_Load(object sender, EventArgs e)14 {15 if (!IsPostBack)16 {17 string strCid = Request.QueryString["cid"];18 if (!string.IsNullOrEmpty(strCid))19 {20 Business.OrderBusiness orderBusiness = new Business.OrderBusiness();21 Business.CustomerBusiness customerBusiness = new Business.CustomerBusiness();22 Order order = new Order() { Customer = customerBusiness.GetCustomerList(c => c.CustomerID == new Guid(strCid)).FirstOrDefault(), OrderDate = DateTime.Now, OrderID = Guid.NewGuid() };23 if (orderBusiness.AddOrder(order))24 {25 Response.Write("添加成功");26 }27 }28 }29 }30 }31 }
添加订单生成的sql
使用单例模式修改NHibernateHelper
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
1 ///2 /// 描述:nhibernate辅助类 3 /// 创建人:wolfy 4 /// 创建时间:2014-10-16 5 /// 6 public class NHibernateHelper 7 { 8 private static ISessionFactory _sessionFactory; 9 private static ISession _session;10 private static object _objLock = new object();11 private NHibernateHelper()12 {13 14 }15 ///16 /// 创建ISessionFactory17 /// 18 ///19 public static ISessionFactory GetSessionFactory()20 {21 if (_sessionFactory == null)22 {23 lock (_objLock)24 {25 if (_sessionFactory == null)26 {27 //配置ISessionFactory28 _sessionFactory = (new Configuration()).Configure().BuildSessionFactory();29 }30 }31 }32 return _sessionFactory;33 34 }35 /// 36 /// 打开ISession37 /// 38 ///39 public static ISession GetSession()40 {41 _sessionFactory = GetSessionFactory();42 if (_session == null)43 {44 lock (_objLock)45 {46 if (_session == null)47 {48 _session = _sessionFactory.OpenSession();49 }50 }51 }52 return _session;53 }54 }
不然有可能出现下面的异常
Illegal attempt to associate a collection with two open sessions级联删除
在级联删除过程中遇到的问题
1,设置Customer.hbm.xml中set属性inverse=“true”,必须设置cascade属性。
12 3 4
此时添加订单,生成的数据如下图所示
当删除CustomerID=‘3042B445-D3AC-4CC0-BDFF-311982E4265A’的客户信息时。
生成的sql语句
1 exec sp_executesql N'DELETE FROM TB_Order WHERE OrderID = @p0',N'@p0 uniqueidentifier',@p0='574BFFF6-D4EC-4EFA-9DBD-32817CD922EC'2 exec sp_executesql N'DELETE FROM TB_Order WHERE OrderID = @p0',N'@p0 uniqueidentifier',@p0='355FF069-FD95-4F33-A6DF-D1AD90CB56D0'3 exec sp_executesql N'DELETE FROM TB_Customer WHERE CustomerID = @p0 AND Version = @p1',N'@p0 uniqueidentifier,@p1 int',@p0='3042B445-D3AC-4CC0-BDFF-311982E4265A',@p1=1
如果不设置cascade属性,会产生如下异常
因为inverse=“true”子实体负责维护关联关系。此时你删除Customer中的记录时,由于在Order表中有对应于该客户的订单信息,会违反外键约束。
cascade(可选):指明哪些操作会从父对象级联到关联的对象。可选all、save-update、delete、none值。除none之外其它将使指定的操作延伸到关联的(子)对象。
2、设置Customer.hbm.xml中set属性inverse=“false”时。
inverse=“false”(默认)父实体负责维护关联关系。
此时如果删除Customer,则属于该客户的订单,会将订单表中的Customerid置为null。
删除CustomerID=‘B0387B7E-C9F3-4970-9429-955DDDC2ACAA’
看一下生成的sql语句
exec sp_executesql N'UPDATE TB_Order SET CustomerID = null WHERE CustomerID = @p0',N'@p0 uniqueidentifier',@p0='B0387B7E-C9F3-4970-9429-955DDDC2ACAA'exec sp_executesql N'DELETE FROM TB_Customer WHERE CustomerID = @p0 AND Version = @p1',N'@p0 uniqueidentifier,@p1 int',@p0='B0387B7E-C9F3-4970-9429-955DDDC2ACAA',@p1=1
此时通过sql语句也可以看到,如果是父实体负责维护关联关系,会在子表中将外键置为null,如果你的外键不允许为null,肯定会有异常的。
如果加上cascade="all"属性
通过生成的sql语句,你会发现此时仍会修改外键为null,然后再去删除
1 exec sp_executesql N'UPDATE TB_Order SET CustomerID = null WHERE CustomerID = @p0',N'@p0 uniqueidentifier',@p0='82724514-682E-4E6F-B759-02E499CDA50F'2 exec sp_executesql N'DELETE FROM TB_Order WHERE OrderID = @p0',N'@p0 uniqueidentifier',@p0='AD7EB390-70BC-4ECF-B593-15A24A40CD26'3 exec sp_executesql N'DELETE FROM TB_Order WHERE OrderID = @p0',N'@p0 uniqueidentifier',@p0='7D21E3FC-CDCF-4586-9714-D6B023CCBFA9'4 exec sp_executesql N'DELETE FROM TB_Customer WHERE CustomerID = @p0 AND Version = @p1',N'@p0 uniqueidentifier,@p1 int',@p0='82724514-682E-4E6F-B759-02E499CDA50F',@p1=2
如果在注重效率的情况下,这中方式没有采用子实体维护关联关系的方式效率高。
通过上面的分析,不管是父实体维护关联关系还是子实体维护关联关系,为映射set节点加上cascade属性是必须的。级联保存
一个简单的例子,在添加客户的时候,直接为用户添加订单。
1 ///2 /// 添加客户和订单 3 /// 4 /// 5 /// 6 protected void btnCsOrder_Click(object sender, EventArgs e) 7 { 8 var customer = new Customer() { NameAddress = new Name() { CustomerName = "zhangsanOrder", CustomerAddress = "北京 海淀" }, CustomerID = Guid.NewGuid() }; 9 customer.Orders = new HashSet();10 customer.Orders.Add(new Order() { OrderID = Guid.NewGuid(), OrderDate = DateTime.Now, Customer = customer });11 customer.Orders.Add(new Order() { OrderID = Guid.NewGuid(), OrderDate = DateTime.Now, Customer = customer });12 Business.CustomerBusiness customerBusiness = new Business.CustomerBusiness();13 if (customerBusiness.AddCustomer(customer))14 {15 this.RepeaterDataBind();16 }17 }
此时的Customer.hbm.xml映射中Set节点的配置如下
生成的sql语句为
你会发现,添加的一个客户信息,对应添加两个Order,添加Order后会执行Update
exec sp_executesql N'INSERT INTO TB_Customer (Version, CustomerName, CustomerAddress, CustomerID) VALUES (@p0, @p1, @p2, @p3)',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 uniqueidentifier',@p0=1,@p1=N'zhangsanOrder',@p2=N'北京 海淀',@p3='4E0C307A-E221-41D5-849D-C6E9F271BD36'exec sp_executesql N'INSERT INTO TB_Order (OrderDate, CustomerID, OrderID) VALUES (@p0, @p1, @p2)',N'@p0 datetime,@p1 uniqueidentifier,@p2 uniqueidentifier',@p0='2014-11-02 14:24:42',@p1='4E0C307A-E221-41D5-849D-C6E9F271BD36',@p2='E44DF3B5-BFD1-4C09-96B5-492D7CAEAADA'exec sp_executesql N'INSERT INTO TB_Order (OrderDate, CustomerID, OrderID) VALUES (@p0, @p1, @p2)',N'@p0 datetime,@p1 uniqueidentifier,@p2 uniqueidentifier',@p0='2014-11-02 14:24:42',@p1='4E0C307A-E221-41D5-849D-C6E9F271BD36',@p2='4E5524D3-48A9-4501-8A52-CD181B843608'exec sp_executesql N'UPDATE TB_Order SET CustomerID = @p0 WHERE OrderID = @p1',N'@p0 uniqueidentifier,@p1 uniqueidentifier',@p0='4E0C307A-E221-41D5-849D-C6E9F271BD36',@p1='E44DF3B5-BFD1-4C09-96B5-492D7CAEAADA'exec sp_executesql N'UPDATE TB_Order SET CustomerID = @p0 WHERE OrderID = @p1',N'@p0 uniqueidentifier,@p1 uniqueidentifier',@p0='4E0C307A-E221-41D5-849D-C6E9F271BD36',@p1='4E5524D3-48A9-4501-8A52-CD181B843608'
这确实有点奇怪了,难道是为了在插入后通过修改外键,来保证这种外键约束吗?
为了一探究竟,对比一下,inverse=true的情况1 2 34 5 6
此时生成的sql
1 exec sp_executesql N'INSERT INTO TB_Customer (Version, CustomerName, CustomerAddress, CustomerID) VALUES (@p0, @p1, @p2, @p3)',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 uniqueidentifier',@p0=1,@p1=N'zhangsanOrder',@p2=N'北京 海淀',@p3='B0720295-9541-40B3-9994-610066224DB8'2 exec sp_executesql N'INSERT INTO TB_Order (OrderDate, CustomerID, OrderID) VALUES (@p0, @p1, @p2)',N'@p0 datetime,@p1 uniqueidentifier,@p2 uniqueidentifier',@p0='2014-11-02 14:33:09',@p1='B0720295-9541-40B3-9994-610066224DB8',@p2='78A53F67-A293-48A1-BBE2-86FED77342FA'3 exec sp_executesql N'INSERT INTO TB_Order (OrderDate, CustomerID, OrderID) VALUES (@p0, @p1, @p2)',N'@p0 datetime,@p1 uniqueidentifier,@p2 uniqueidentifier',@p0='2014-11-02 14:33:09',@p1='B0720295-9541-40B3-9994-610066224DB8',@p2='51CA6A37-EC79-4613-B8D7-CEEF5A4BB8EE'
可以通过子实体来维护级联关系,更符合程序员的逻辑习惯,并且效率上更好一些。
总结
a)
inverse="false" cascade="all" 结果:Customer和Order都删了 b) inverse="true" cascade="all" 结果:Customer和Order都删了 c) 不设置inverse 不设置cascade 结果:Customer删除了,Order的Customer设为了空 d) 不设置inverse 设置cascade ="delete" 结果:Customer和Order都删了 e) 不设置inverse 设置cascade ="save-update" 结果:Customer删除了,Order的Customer设为了空 f) 不设置inverse 设置cascade ="all-delete-orphan" 结果:Customer和Order都删了通过上面的级联删除与添加分析,建议使用inverse="true" cascade="all"的情况,因为此时更符合咱们的思维方式,效率上也更好。
参考文章