NHibernate学习手记(6) - 实现one2many/many2one的映射
一对多(one2many)是最常见的对象关系之一,本文将通过示例说明如何使用NH来实现one2many关系的映射,以及如何实现Parent/Child对象之间的级连操作。
根据约定,本文将通过Category和Item对象来描述one2many的关系,即一个Category对象对应多个Item对象。
主要内容:
1、编写POCO类
2、准备数据库
3、编写配置文件
4、级连(cascading)操作示例
一、编写POCO类
从手记(6)起我打算由编写POCO类开始描述,因为NHibernate已经让我们以对象的方式去思考数据操作,数据表该怎么设计已经不是思维的起点,更不是重点。
1、Category的POCO类:
2 using System.Collections;
3
4 namespace TestOne2Many
5 {
6 /// <summary>
7 /// Category 的摘要说明
8 /// </summary>
9 /// 创 建 人: Aero
10 /// 创建日期: 2006-3-17
11 /// 修 改 人:
12 /// 修改日期:
13 /// 修改内容:
14 /// 版 本:
15 public class Category
16 {
17 private Guid _categoryId;
18
19 private string _name = string.Empty;
20
21 private IList _items;
22
23 public Guid CategoryID
24 {
25 get { return this._categoryId; }
26 set { this._categoryId = value; }
27 }
28
29 public string Name
30 {
31 get { return this._name; }
32 set { this._name = value; }
33 }
34
35 public IList Items
36 {
37 get { return this._items; }
38 set { this._items = value; }
39 }
40
41 #region 构造函数
42 /// <summary>
43 /// 默认无参构造函数
44 /// </summary>
45 /// 创 建 人: Aero
46 /// 创建日期: 2006-3-17
47 /// 修 改 人:
48 /// 修改日期:
49 /// 修改内容:
50 public Category()
51 {
52 this._items = new ArrayList();
53 }
54
55 #endregion
56 }
57 }
58
一对多的关系在.net代码里是以集合的形式去表示的,按照NH的online document的说法,NH支持三种类型的集合:System.Collections.IList、System.Collection.IDictionary、Iesi.Collections.ISet,在进行O/R mapping时,NH将自动把上述集合转化为NHibernate.Collection中对应的集合类型。下面的测试代码无法通过,因为NH已经把Category.Items转化为NHibernate.Collection.Bag
2 public void TestCollectionType()
3 {
4 using (ISession session = TestCategory.Factory.OpenSession())
5 {
6 // We assume that there are only one category object in the repository,
7 // see initialization in TestCategory.TestInitialize().
8 // note: cascading option should set as "all-delete-orphan" in objects.hbm.xml
9 Category expectedCategory =
10 session.CreateCriteria(typeof(Category)).List()[0] as Category;
11
12 // that works?
13 Assert.AreEqual(expectedCategory.Items.GetType(), typeof(System.Collections.ArrayList));
14 }
15 }
2、Item的POCO类:
2
3 namespace TestOne2Many
4 {
5 /// <summary>
6 /// Item 的摘要说明
7 /// </summary>
8 /// 创 建 人: Aero
9 /// 创建日期: 2006-3-17
10 /// 修 改 人:
11 /// 修改日期:
12 /// 修改内容:
13 /// 版 本:
14 public class Item
15 {
16 private Guid _itemId;
17
18 private string _name = string.Empty;
19
20 private Category _category;
21
22 public Guid ItemID
23 {
24 get { return this._itemId; }
25 set { this._itemId = value; }
26 }
27
28 public string Name
29 {
30 get { return this._name; }
31 set { this._name = value; }
32 }
33
34 public Category Category
35 {
36 get { return this._category; }
37 set { this._category = value; }
38 }
39
40 #region 构造函数
41 /// <summary>
42 /// 默认无参构造函数
43 /// </summary>
44 /// 创 建 人: Aero
45 /// 创建日期: 2006-3-17
46 /// 修 改 人:
47 /// 修改日期:
48 /// 修改内容:
49 public Item()
50 {
51 //
52 // TODO: 在此处添加构造函数逻辑
53 //
54 }
55
56 #endregion
57 }
58 }
59
二、准备数据库
新建数据库nh_categories和nh_items,数据库设计如下:
或直接执行以下sql语句(本文示例代码所使用的数据库名称为NHTrial)
GO
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[nh_categories_nh_items_FK1]') and OBJECTPROPERTY(id, N'IsForeignKey') = 1)
ALTER TABLE [dbo].[nh_items] DROP CONSTRAINT nh_categories_nh_items_FK1
GO
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[nh_categories]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[nh_categories]
GO
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[nh_items]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[nh_items]
GO
CREATE TABLE [dbo].[nh_categories] (
[CategoryID] [uniqueidentifier] NOT NULL ,
[Name] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NOT NULL
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[nh_items] (
[ItemID] [uniqueidentifier] NOT NULL ,
[CategoryID] [uniqueidentifier] NOT NULL ,
[Name] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NOT NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[nh_categories] WITH NOCHECK ADD
CONSTRAINT [nh_categories_PK] PRIMARY KEY CLUSTERED
(
[CategoryID]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[nh_items] WITH NOCHECK ADD
CONSTRAINT [nh_items_PK] PRIMARY KEY CLUSTERED
(
[ItemID]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[nh_items] ADD
CONSTRAINT [nh_categories_nh_items_FK1] FOREIGN KEY
(
[CategoryID]
) REFERENCES [dbo].[nh_categories] (
[CategoryID]
)
GO
三、编写配置文件
1、新建hibernate.cfg.xml文件,设置hibernate的运行配置信息。
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.0" >
<session-factory name="TestOne2Many">
<!-- properties -->
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
<property name="connection.connection_string">Server=localhost;database=NHTrial;User Id=sa;Password=sa</property>
<property name="show_sql">false</property>
<property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
<property name="use_outer_join">true</property>
</session-factory>
</hibernate-configuration>
2、新建文件objects.hbm.xml,配置Category和Item类的o/r mapping信息,并且把objectes.hbm.xml的属性设置为“嵌入资源”。
子非鱼在NHibernate学习里说实体xxx的mapping信息要写在xxx.hbm.xml文件里面,其实nhibernate没有这个限制,完全可以把n个实体的配置信息写在一个hbm.xml文件中。
2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
3 <class name="TestOne2Many.Category, TestOne2Many" table="nh_categories">
4 <id name="CategoryID" column="CategoryID" type="Guid"
5 unsaved-value="00000000-0000-0000-0000-000000000000">
6 <generator class="guid" />
7 </id>
8
9 <property name="Name" type="string" length="50" />
10 <bag name="Items" lazy="true" inverse="true" cascade= "all-delete-orphan">
11 <key column="CategoryID" />
12 <one-to-many class="TestOne2Many.Item, TestOne2Many" />
13 </bag>
14 </class>
15
16 <class name="TestOne2Many.Item, TestOne2Many" table="nh_items">
17 <id name="ItemID" column="ItemID" type="Guid"
18 unsaved-value="00000000-0000-0000-0000-000000000000">
19 <generator class="guid" />
20 </id>
21
22 <property name="Name" type="string" length="50" />
23 <many-to-one name="Category" class="TestOne2Many.Category, TestOne2Many"
24 column="CategoryID" />
25 </class>
26
27 </hibernate-mapping>
28
部分配置节点的含义和用法在NHibernate学习手记(5) - 简单的对象映射里已经说过了,这里只看看bag、one-to-many和many-to-one。
1)bag节点:用于定义System.Collection.IList的类型的集合元素。
Attributes |
Usage |
Example
|
name |
指示映射的属性名称。Required |
Items (指Category.Items) |
lazy |
指示是否使用延迟加载。Optional |
true | false |
cascade |
指示级连操作类型。Optional |
all |
2)级连操作选项说明:
value |
usage |
||
none |
默认值,不进行级连操作。 |
||
save-update |
进行级连save和update操作 |
||
delete |
进行级连删除操作 |
||
delete-orphan |
删除无相关的父对象的子对象 |
||
all |
进行级连save/update/delete操作 |
||
all-delete-orphan |
all + delete-orphan |
当进行save-update级连操作时,NH将根据子对象主键的unsave-value来判断该执行save还是update操作。
3)key节点:用于指定nh_items表中用作外键(和nh_categories)的数据列名称
4)one-to-many节点:用于指定子对象的类型(全限定名称)
5)many-to-one节点:用于指定父对象属性,如Item.Category
Attributes |
Usage |
Example
|
name |
指示映射的属性名称。Required |
Category (指Item.Category) |
class |
指示指示父对象的全限定名称。Required |
TestOne2Many.Category,
TestOne2Many |
column |
指示子表的外键列名称。Required |
CategoryID |
四、示例one2many的级连操作。
看过子非鱼兄的NHibernate学习后,发现用单元测试来进行代码示例的确是一种非常有效的方式,大家只需要的是再增加一个NUnit.framework的引用:)。
1、级连添加:在保存新增的Category对象时,级连保存与之关联的Item对象
2 /// demonstrate how to execute a the cascading save
3 /// </summary>
4 [Test]
5 public void TestCascadingSave()
6 {
7 using (ISession session = TestCategory.Factory.OpenSession())
8 {
9 // prepare test objects
10 Category expectedCategory = new Category();
11 expectedCategory.Name = "category" + System.Environment.TickCount.ToString();
12
13 for (int i = 0; i < 10; i++)
14 {
15 Item item = new Item();
16 item.Name = "item" + System.Environment.TickCount.ToString();
17 item.Category = expectedCategory;
18
19 expectedCategory.Items.Add(item);
20 }
21
22 // save objects in a all-cascading way
23 // note: cascading option should at least set as "all" in objects.hbm.xml
24 ITransaction trans = session.BeginTransaction();
25
26 try
27 {
28 session.SaveOrUpdate(expectedCategory);
29 trans.Commit();
30 }
31 catch
32 {
33 trans.Rollback();
34 throw;
35 }
36
37 // that works?
38 Category actualCategory =
39 session.Get(typeof(Category), expectedCategory.CategoryID) as Category;
40 Assert.IsNotNull(actualCategory);
41 Assert.AreEqual(expectedCategory.Items.Count, actualCategory.Items.Count);
42 }
43 }
2、级连更新(update):移除Category对象的部分Item子对象,保存更改后,被移除的Item子对象从数据库中删除。
2 /// demonstrate how to remove sub-items and execute a cascading update
3 /// </summary>
4 [Test]
5 public void TestCascadingUpdate()
6 {
7 using (ISession session = TestCategory.Factory.OpenSession())
8 {
9 // We assume that there are only one category object in the repository,
10 // see initialization in TestCategory.TestInitialize().
11 // note: cascading option should set as "all-delete-orphan" in objects.hbm.xml
12 Category expectedCategory =
13 session.CreateCriteria(typeof(Category)).List()[0] as Category;
14 int expectedItemCount = expectedCategory.Items.Count;
15
16 // execute a cascading update
17 ITransaction trans = session.BeginTransaction();
18
19 try
20 {
21 // remove an item from item-collection from the repository
22 expectedCategory.Items.RemoveAt(0);
23
24 session.Update(expectedCategory);
25 trans.Commit();
26 }
27 catch (System.Exception e)
28 {
29 trans.Rollback();
30 throw;
31 }
32
33 // that works?
34 Assert.AreEqual(1, session.CreateCriteria(typeof(Category)).List().Count);
35 Assert.AreEqual(expectedItemCount - 1, session.CreateCriteria(typeof(Item)).List().Count);
36 }
37 }
3、级连删除:当父Category对象被删除,与之关联的Item对象也被删除。
2 /// demonstrate how to execute a cascading deletion
3 /// </summary>
4 [Test]
5 public void TestCascadingDelete()
6 {
7 using (ISession session = TestCategory.Factory.OpenSession())
8 {
9 // We assume that there are only one category object in the repository,
10 // see initialization in TestCategory.TestInitialize().
11 // note: cascading option should set as "all-delete-orphan" in objects.hbm.xml
12 Category expectedCategory =
13 session.CreateCriteria(typeof(Category)).List()[0] as Category;
14
15 // remove category from the repository
16 ITransaction trans = session.BeginTransaction();
17
18 try
19 {
20 session.Delete(expectedCategory);
21 trans.Commit();
22 }
23 catch (System.Exception e)
24 {
25 trans.Rollback();
26 throw;
27 }
28
29 // that works?
30 Assert.AreEqual(0, session.CreateCriteria(typeof(Category)).List().Count);
31 Assert.AreEqual(0, session.CreateCriteria(typeof(Item)).List().Count);
32 }
33 }
要特殊指出的是,NH不支持通过Category.Item=null这种方式来删除与Category对象关联的Item对象。
2 public void TestCascadingUpdateFail()
3 {
4 using (ISession session = TestCategory.Factory.OpenSession())
5 {
6 // We assume that there are only one category object in the repository,
7 // see initialization in TestCategory.TestInitialize().
8 // note: cascading option should set as "all-delete-orphan" in objects.hbm.xml
9 Category expectedCategory =
10 session.CreateCriteria(typeof(Category)).List()[0] as Category;
11 int expectedItemCount = expectedCategory.Items.Count;
12
13 // execute a cascading update
14 ITransaction trans = session.BeginTransaction();
15
16 try
17 {
18 // we can't remove all items by dereference Category.Items as null,
19 // this will cause a NHiberate.HibernateException
20 expectedCategory.Items = null;
21
22 // still we can't remove items in the following way,
23 // this will cause a NullReference Exception from NHibernate
24
25 //expectedCategory.Items[0] = null;
26
27 session.Update(expectedCategory);
28 trans.Commit();
29 }
30 catch (System.Exception e)
31 {
32 trans.Rollback();
33 throw;
34 }
35
36 // that works?
37 Assert.AreEqual(1, session.CreateCriteria(typeof(Category)).List().Count);
38 Assert.AreEqual(expectedItemCount, session.CreateCriteria(typeof(Item)).List().Count);
39 }
40 }
完整示例代码可从ObjectMappings.rar下载,其中的TestOne2Many即本文所讨论的工程。
All the posts in this blog are provided "AS IS" with no warranties, and confer no rights. Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution 2.5 China Mainland License.