摘要
这篇文章介绍NHibernate最实用的内容:关系映射。
NHibernate的关系映射方式有三种:
Set:无序对象集合,集合中每一个元素不能重复。
List:有序对象集合,集合中的元素可以重复。
Bag:无序对象集合,集合中的元素可以重复。
Map:键值对集合,相当于Hashtable或Dictionary。
这篇文章以一对多关系为例,介绍怎样在NHibernate中建立一对多关系映射。一对多关系是在现实项目中最经常碰到的一种关系。后面文章介绍多对多关系。
这篇文章的附件:NHibernate Demo下载。
1. 建立数据库关系
创建Order表
创建Order表的SQL语句:
USE [NHibernateDemoDB] GO IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_Order_Customer]') AND parent_object_id = OBJECT_ID(N'[dbo].[Order]')) ALTER TABLE [dbo].[Order] DROP CONSTRAINT [FK_Order_Customer] GO USE [NHibernateDemoDB] GO /****** Object: Table [dbo].[[Order]] Script Date: 06/28/2016 10:38:37 ******/ IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Order]') AND type in (N'U')) DROP TABLE [dbo].[Order] GO USE [NHibernateDemoDB] GO /****** Object: Table [dbo].[[Order]] Script Date: 06/28/2016 10:38:37 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Order]( [Id] [int] IDENTITY(1,1) NOT NULL, [CustomerId] [int] NULL, [Ordered] [datetime] NULL, [Shipped] [datetime] NULL, [Street] [nvarchar](100) NULL, [City] [nvarchar](100) NULL, [Province] [nvarchar](100) NULL, [Country] [nvarchar](100) NULL, CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Order] WITH CHECK ADD CONSTRAINT [FK_Order_Customer] FOREIGN KEY([CustomerId]) REFERENCES [dbo].[Customer] ([Id]) GO ALTER TABLE [dbo].[Order] CHECK CONSTRAINT [FK_Order_Customer] GO
Customer表和Order表的关系:
2. 创建Order类,修改Customer类
Order类:
1 using System; 2 3 namespace Demo.XML.Entities.Domain 4 { 5 public class Order 6 { 7 public virtual int Id { get; set; } 8 public virtual DateTime Ordered { get; set; } 9 public virtual DateTime? Shipped { get; set; } 10 public virtual Address ShipTo { get; set; } 11 public virtual Customer Customer { get; set; } 12 } 13 }
Customer属性表示一个Order对象从属于一个Customer对象。
Customer类:
1 using System; 2 using System.Collections.Generic; 3 4 namespace Demo.XML.Entities.Domain 5 { 6 public class Customer 7 { 8 public Customer() 9 { 10 MemberSince = DateTime.UtcNow; 11 Orders = new HashSet<Order>(); 12 } 13 14 public virtual int Id { get; set; } 15 public virtual string FirstName { get; set; } 16 public virtual string LastName { get; set; } 17 public virtual double AverageRating { get; set; } 18 public virtual int Points { get; set; } 19 public virtual bool HasGoldStatus { get; set; } 20 public virtual DateTime MemberSince { get; set; } 21 public virtual CustomerCreditRating CreditRating { get; set; } 22 public virtual Address Address { get; set; } 23 public virtual ISet<Order> Orders { get; set; } 24 25 public virtual void AddOrder(Order order) 26 { 27 Orders.Add(order); 28 order.Customer = this; 29 } 30 } 31 32 public enum CustomerCreditRating 33 { 34 Excellent, VeryVeryGood, VeryGood, Good, Neutral, Poor, Terrible 35 } 36 }
- 为Customer类添加了一个无参数的构造函数,为实体对象的属性赋默认值。
- 实体类的方法必须是virtual修饰的。
- Orders属性是ISet类型,表示一个Customer对象有一个Order集合。
- 这里示例使用的是双向关联,如果你愿意也可以写单向关联。
3. 添加Order.hbm.xml文件,修改Customer.hbm.xml文件
Order.hbm.xml:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Demo.XML.Entities" namespace="Demo.XML.Entities.Domain"> <class name="Order" table="`Order`"> <id name="Id"> <generator class="native"/> </id> <property name="Ordered"/> <property name="Shipped"/> <component name="ShipTo"> <property name="Street"/> <property name="City"/> <property name="Province"/> <property name="Country"/> </component> <many-to-one name="Customer" column="CustomerId" cascade="save-update"/> </class> </hibernate-mapping>
在多的这一端使用:
<many-to-one name="属性名" column="属性对应的列名" />
这里的属性是Customer
Customer.hbm.xml:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Demo.XML.Entities" namespace="Demo.XML.Entities.Domain"> <class name="Customer" table="Customer"> <id name="Id"> <generator class="native"/> </id> <property name="FirstName" not-null="true"/> <property name="LastName" not-null ="true"/> <property name="AverageRating"/> <property name="Points"/> <property name="HasGoldStatus"/> <property name="MemberSince"/> <property name="CreditRating" type="CustomerCreditRating"/> <component name="Address"> <property name="Street"/> <property name="City"/> <property name="Province"/> <property name="Country"/> </component> <set name="Orders" table="`Order`" cascade="all-delete-orphan"> <key column="CustomerId"/> <one-to-many class="Order"/> </set> </class> </hibernate-mapping>
在一的这一端使用:
<set name="属性名" table="Many那端的表名">
<key column="Many那端的外键对应的列名"/>
<one-to-many class="Many那端的类名"/>
</set>
因为Customer类的Orders属性类型是ISet,所以这里使用Set。对应的,如果Orders类型是IList,则使用List。
cascade表示主从表的级联关系,有五个取值:
- none:没有任何级联操作
- all:对save、update和delete操作都产生级联
- delete:删除主表记录,级联删除从表记录
- save-update:对save和update操作产生级联
- all-delete-orphan:对save、update和delete操作都产生级联,并且在删除主表记录时删除从表的“孤立”(没有外键关联)记录。
在主表方的映射文件设置casade:主表的操作怎样关联到从表。
在从表方的映射文件设置casade:从表的操作怎样关联到主表。
如果不设置主从表的级联,则需要既要对主表的操作写Save/Update代码,又要对从表的操作写Save/Update代码,这样就非常麻烦。
4、添加ICustomerService接口、CustomerService类。
1 using Demo.Service.Infrastructure.Interface; 2 using Demo.XML.Entities.Domain; 3 4 namespace Demo.Service.Interface 5 { 6 public interface ICustomerService : IService<Customer> 7 { 8 } 9 }
ICustomerService继承IService<Customer>泛型接口,继承了最常用的QueryAll/Save/Update/Delete等方法,还可以添加Customer业务自身的方法接口。
1 using Demo.Service.Interface; 2 using Demo.Service.Infrastructure; 3 using Demo.XML.Entities.Domain; 4 5 namespace Demo.Service 6 { 7 public class CustomerService : Service<Customer>, ICustomerService 8 { 9 } 10 }
按照上面的接口和类,添加IOrderService接口和OrderService类。
5. 关联映射关系的添加、修改、删除
1)添加Customer对象,级联添加Order对象
1 using Demo.Service; 2 using Demo.Service.Interface; 3 using Demo.XML.Entities.Domain; 4 using System; 5 6 namespace Demo.ConsoleApp 7 { 8 class Program 9 { 10 static readonly ICustomerService customerService = new CustomerService(); 11 static readonly IOrderService orderService = new OrderService(); 12 13 static void Main(string[] args) 14 { 15 HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize(); 16 17 var newCustomer = CreateCustomer(); 18 Console.WriteLine("New Customer:"); 19 customerService.Save(newCustomer); 20 int id = newCustomer.Id; 21 22 var list = customerService.GetAll(); 23 24 Console.WriteLine("Completed"); 25 Console.ReadLine(); 26 } 27 28 private static Customer CreateCustomer() 29 { 30 var customer = new Customer 31 { 32 FirstName = "Daniel", 33 LastName = "Tang", 34 Points = 100, 35 HasGoldStatus = true, 36 MemberSince = new DateTime(2012, 1, 1), 37 CreditRating = CustomerCreditRating.Good, 38 AverageRating = 42.42424242, 39 Address = new Address 40 { 41 Street = "123 Somewhere Avenue", 42 City = "Nowhere", 43 Province = "Alberta", 44 Country = "Canada" 45 } 46 }; 47 48 var order1 = new Order 49 { 50 Ordered = DateTime.Now 51 }; 52 customer.AddOrder(order1); 53 var order2 = new Order 54 { 55 Ordered = DateTime.Now.AddDays(-1), 56 Shipped = DateTime.Now, 57 ShipTo = new Address 58 { 59 Street = "123 Somewhere Avenue", 60 City = "Nowhere", 61 Province = "Alberta", 62 Country = "Canada" 63 } 64 }; 65 customer.AddOrder(order2); 66 67 return customer; 68 } 69 } 70 }
执行程序,Customer表和Order表记录都添加成功。
2)从Customer对象上清除Orders,再添加Order对象
1 static void Main(string[] args) 2 { 3 HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize(); 4 5 var customer = customerService.GetById(1); 6 if (customer.Orders.Count > 0) 7 { 8 customer.Orders.Clear(); 9 } 10 customer.AddOrder(new Order { 11 Ordered = DateTime.Now.AddDays(-1), 12 Shipped = DateTime.Now, 13 ShipTo = new Address 14 { 15 Street = "Zhuhai Road", 16 City = "Zhuhai", 17 Province = "Guangdong", 18 Country = "China" 19 } 20 }); 21 customerService.Update(customer); 22 23 Console.WriteLine("Completed"); 24 Console.ReadLine(); 25 }
执行程序,得到监控结果。先插入新的Order记录,然后修改数据库内三条Order记录的CustomerId值,已存在的两条Order记录的CustomerId设置成null,新添加的这一条Order记录设置CustomerId为Customer主键值。最后删除之前的两条Order记录。
3)删除Customer对象
1 static void Main(string[] args) 2 { 3 HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize(); 4 5 customerService.Delete(1); 6 7 Console.WriteLine("Completed"); 8 Console.ReadLine(); 9 }
执行程序,得到监控结果。先修改Order记录的CustomerId为null,然后删除Order对象,最后删除Customer对象。
4)添加Order对象,级联添加Customer对象
1 static void Main(string[] args) 2 { 3 HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize(); 4 5 var customer = CreateCustomer(); 6 var order = new Order 7 { 8 Ordered = DateTime.Now 9 }; 10 customer.AddOrder(order); 11 orderService.Save(order); 12 13 Console.WriteLine("Completed"); 14 Console.ReadLine(); 15 }
执行程序,得到监控结果。先插入Customer对象,再插入三条Order对象,虽然插入的三条Order对象的CustomerId为Customer记录的主键值,但是还是在后面执行了三条修改CustomerId的update语句。
5)修改Order对象所属的Customer对象
手动向数据库Customer表添加一条Customer记录,假设新生成的记录的Id为3。
1 static void Main(string[] args) 2 { 3 HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize(); 4 5 var customer = customerService.GetById(3); 6 var order = orderService.GetById(4); 7 order.Customer = customer; 8 orderService.Update(order); 9 10 Console.WriteLine("Completed"); 11 Console.ReadLine(); 12 }
执行程序,得到监控结果。
5、inverse
通过监控看到上面的关联关系的添加修改删除操作执行了不少非必要的update操作。如果使用inverse属性,可以省去不少不必要的update执行语句。
修改Customer.hbm.xml文件。
<set name="Orders" table="`Order`" cascade="all-delete-orphan" inverse="true"> <key column="CustomerId"/> <one-to-many class="Order"/> </set>
重建数据库表Customer表和Order表,重新执行第四节的步骤1:“添加Customer对象,级联添加Order对象”,得到监控结果。
重新执行步骤2:“从Customer对象上清除Orders,再添加Order对象”,得到监控结果。
重新执行步骤3:“删除Customer对象”,得到监控结果。
重新执行步骤4:“添加Order对象,级联添加Customer对象”,得到监控结果。
重新执行步骤5:“修改Order对象所属的Customer对象”,得到监控结果。
因为在这个示例里,定义了双向关联,NHibernate需要维护双向之间的关联关系。所以没加inverse属性之前,添加修改删除生成了很多不必要的update语句。
“inverse=true”(默认值是false)的作用是让NHibernate忽略掉其中一个方向的关联,加在主表一方的时候,忽略从表到主表的关联,只维护主表到从表的关联,因此去掉了那些不必要的update语句。
读者也可以试着将“inverse=true”加在从表一方,看生成的监控结果是怎样的。
6、设置关系属性为null
1)修改Order对象的Customer属性为null。
1 var order = orderService.GetById(4); 2 order.Customer = null; 3 orderService.Update(order);
上面3行代码的结果是将Order对象的Customer属性设为null,执行后将Order表记录的CustomerId设置为null。
在添加Order记录的时候,设置Customer属性为空也是可以的,但是通常这没有什么意义。
也可以在Order.hbm.xml文件内定义Customer属性不能为空。如果为空,在Insert/Update时,会抛出异常。
<many-to-one name="Customer" column="CustomerId" cascade="save-update" not-null="true"/>
2)在Customer对象中的Orders集合调用Clear方法,然后Update这个Customer对象,此时将删除Customer对象关联的Order对象。
结语
这篇文章介绍了NHibernate关系映射概念,有四种集合可以实现关系映射:Set、Bag、List和Map。以1-many为例介绍了怎样在NHibernate映射文件和实体类中中建立1-many映射。用一个示例介绍了主从表的级联关系,级联关系中的添加修改删除操作。最后介绍了inverse属性以及作用,inverse的作用是在双向关联的关系中,让NHibernate忽略掉其中一个方向的关联,少执行很多不必要的update语句。
下一篇文章介绍NHibernate关系映射多对多映射。