NHibernate初学者指南(5):映射模型到数据库之方式一
映射类型
当使用Nhibernate作为我们的ORM框架时,有四种主要的映射类型:
- 基于XML的映射。
- 基于特性的映射。
- Fluent映射。
- 基于约定的映射,有时也称为自动映射。
在接下来的三篇文章里,将详细讲解除“基于特性映射”以外的映射类型。
Fluent映射
本篇文章的所有操作需要在NHibernate初学者指南(3):创建Model中创建的代码基础上完成,代码下载地址:点击这里下载。
为了能够使用Fluent方式映射我们的模型到底层的数据库,需要添加Fluent NHibernate和NHibernate两个引用,这两个程序集可以在NHibernate初学者指南(2):一个完整的例子中创建的lib文件中找到。
映射类
要映射实体到数据库表,必须往我们的项目里添加一个继承自ClassMap<T>(定义在FluentNHibernate.dll程序集中)的类。例如映射Product实体,我们应该定义一个如下面代码的映射类:
public class ProductMap : ClassMap<Product> { public ProductMap() { // 这里定义映射信息 } }
映射信息必须定义在映射类的默认构造函数中。注意ClassMap<T>要求映射的类作为泛型参数,本例中是Product类。
实体级别设置
如果想让数据库理解领域模型,我们需要指定最起码的详细信息。然而,为了额外的灵活性,我们有能力明确指定更多的详细信息。作为一个例子,如果映射到的表名称不同于实体的类名称,我们可以定义表的名称。可以使用下面的代码定义:
public ProductMap() { // 这里定义映射信息 Table("tbl_Product"); }
如果一个特定的表驻留在另一个架构(schema)中,我们还可以使用下面的代码指定相应的架构:
public ProductMap() { // 这里定义映射信息 //Table("tbl_Product"); Schema("OrderingSystem"); }
默认情况下,Fluent NHibernate配置实体为延迟加载(lazy loaded)。如果不喜欢这样,可以使用下面的代码改变默认的行为:
public ProductMap() { // 这里定义映射信息 //Table("tbl_Product"); //Schema("OrderingSystem"); Not.LazyLoad(); }
还有更多的信息可以指定,但不在本次介绍的范围之内。
ID列
通常我们首先要映射的是实体的ID。这需要使用Id方法完成。这个方法有很多选项,使用最简单的形式,如下面的代码:
public ProductMap() { Id(x => x.ID).GeneratedBy.HiLo("1000"); }
使用上面的代码,我们就告诉了NHibernate,Product实体的Id属性应该作为主键映射,新的ID会由NHibernate的HiLo生成器自动生成。只要数据库里我们的字段为Id,一切处理正常。然而,如果命名为ID或PRODUCT_ID呢?答案是根本就不会处理了。这种情况下,必须添加可选的列参数指定列的名称:
Id(x => x.ID, "PRODUCT_ID").GeneratedBy.HiLo("1000");
另外,还可以写成下面的形式:
Id(x => x.ID).Column("PRODUCT_ID").GeneratedBy.HiLo("1000");
ID还有一个经常使用的可选属性:UnsavedValue。它指定一个新对象在持久化到数据库之前应该返回什么值。如下面的代码:
Id(x => x.ID, "PRODUCT_ID").GeneratedBy.HiLo("1000").UnsavedValue(-1);
注意我们没有显示的指定ID的类型。Fluent NHibernate可以通过反射获得它的类型。总之,我们应该避免指定冗余的信息,因为没有必要,只会使你的代码杂乱。
属性
映射简单的值属性,我们使用Map方法。简单值属性是含有基本.NET类型之一的属性,如int,float,double,decimal,bool,string或DateTime。这样的属性简单的映射到数据库列。例如,Product实体的Name属性,使用最简单的形式,如下面的代码所示:
Map(x => x.Name);
上面的代码告诉NHibernate映射Name属性到数据库中有相同名字的列。SQL Server中数据库列的类型是nvarchar,数据库列的最大长度是255。此外,列是可空的。
如果列的最大长度为50,不为空,则如下面代码所示:
Map(x => x.Name).Length(50).Not.Nullable();
如果数据库列的名字和属性的名字不同,可以使用下面的代码指定,跟Id是一样的:
Map(x => x.Name, "PRODUCT_NAME").Length(50).Not.Nullable();
有时,NHibernate不能自动的理解我们如何映射某个属性,必须指定我们的意图。作为例子,我们想映射一个枚举类型的属性。假设实体Product有一个ProductTypes类型的属性ProducType,ProductTypes是一个枚举,如下:
public enum ProductTypes { ProductTypeA, ProductTypeB }
然后我们可以添加一个CustomType方法映射这个属性,如下:
Map(x => x.ProductType).CustomType<ProductTypes>();
在这种情况下,因为enum背后的类型是整型(默认是int),对SQL Server来说数据库字段会是int类型的。NHibernate会自动的在领域模型的enum值和数据库中的int类型的数字之间转换。
另一个经常使用的是将bool类型的属性映射为数据库中char(1)类型的列,true就是'Y',false是'N'。NHibernate为此定义了一个特殊的映射,称为YesNo。例如人,我们可以使用下面的代码映射Product实体的Discontinued属性:
Map(x => x.Discontinued).CustomType("YesNo");
Fluent NHibernate非常完整,为我们映射简单值属性提供了很大的灵活性。在本节中,只讨论了最重要和最常用的设置。
引用
映射引用另一个实体的属性,如Product实体的Category属性,可以使用下面的代码简单的映射这种关系:
References(x => x.Category);
这种类型的映射表示“多对一”关系,在领域模型中经常使用。
大多数情况下,我们想强制定义这个引用,可以使用下面的代码:
References(x => x.Category).Not.Nullable();
如果我们不显示指定,Product表到Category表的外键名称会是Category_Id,默认约定是属性的名称和ID用下划线组合的。当然,可以使用下面的代码修改外键的名称:
References(x => x.Category).Not.Nullable().ForeignKey("ProductId");
最后一个值得注意的设置是指定引用为唯一的。如下面的代码:
References(x => x.Category).Not.Nullable().Unique();
集合
现在说说一对多关系。让我们看一个订单包含line item对象的集合的例子。line item集合的映射如下面的代码:
public OrderMap()
{
HasMany(x => x.LineItems);
}
通常定义HasMany映射为Inverse。在order和line items的一对多关系中,如果不标记集合为inverse,那么NHibernate会执行一个额外的UPDATE操作。当标记为Inverse,那么NHibernate会首先持久化拥有集合的实体(order),然后持久化集合中的实体(line items)。避免了额外的UPDATE操作。如下面的代码:
HasMany(x => x.LineItems).Inverse();
还有一个重要的配置就是NHibernate的级联操作。大多数情况下,会使用这个配置,如下面的代码:
HasMany(x => x.LineItems).Inverse().Cascade.AllDeleteOrphan();
映射多对多关系
我们还可以映射多对多关系。在领域模型中,Book实体包含Author子对象的集合,同时,Author实体也包含Book子对象的集合。定义这样的映射代码如下:
HasMany(x =>x.Author);
如果指定中间表的名称,如下面的代码:
HasMany(x => x.Author).Table("BookAuthor");
映射值对象
在NHibernate初学者指南(3):创建Model一篇中,我们了解到值对象是领域模型非常重要的概念,所以,我们需要知道如何映射含有值对象作为类型的属性。拿Customer实体的Name属性作为例子。首先,我们可能想映射值对象自身。我们定义一个NameMap类,它继承自ComponentMap<Name>,如下面代码所示:
public class NameMap : ComponentMap<Name> { ...}
然后,在这个类的默认构造函数中添加映射的详细信息,如下面代码所示:
public NameMap()
{
Map(x => x.LastName).Not.Nullable().Length(50);
Map(x => x.MiddleName).Length(50);
Map(x => x.FirstName).Not.Nullable().Length(50);
}
一旦映射了值对象,我们就可以定义Customer实体的Name属性的映射。在CustomerMap类的构造函数中添加如下代码:
Component(x => x.CustomerName);
讲完了Fluent映射所需的基本知识,下面完成我们模型的映射。
实战时间–映射我们的Model
1. 按照本篇文章开始的要求,下载源码,添加引用。
2. 在NHibernate初学者指南(3):创建Model中我们使用了GUID类型的ID,这一篇中,我们想使用int类型的ID,打开Entity<T>类,修改属性ID为:
public int ID { get; private set; }
3. 将Entity<T>基类中的Guid.Empty使用0(零)替换。
4. 在项目中新建一个文件夹Mappings。
5. 映射Employee实体,新建一个EmployeeMap类,继承自ClassMap<Employee>,代码如下:
public class EmployeeMap : ClassMap<Employee> { public EmployeeMap() { Id(x => x.ID).GeneratedBy.HiLo("100"); Component(x => x.Name); } }
6. 映射Name值对象,新建NameMap类,继承自ComponentMap<Name>,代码如下:
public class NameMap : ComponentMap<Name> { public NameMap() { Map(x => x.LastName).Not.Nullable().Length(100); Map(x => x.MiddleName).Length(100); Map(x => x.FirstName).Not.Nullable().Length(100); } }
7. 映射Customer实体,新建一个CustomerMap类,继承自ClassMap<Customer>,代码如下:
public class CustomerMap : ClassMap<Customer> { public CustomerMap() { Id(x => x.ID).GeneratedBy.HiLo("100"); Component(x => x.CustomerName); Map(x => x.CustomerIdentifier).Not.Nullable().Length(50); Component(x => x.Address); HasMany(x => x.Orders).Access.CamelCaseField().Inverse().Cascade.AllDeleteOrphan(); } }
这里需要解释一下,为了使NHibernate直接访问Customer类的私有字段orders,使用了Access.CamelCaseField()。Cascade.AllDeleteOrphan()告诉NHibernate从customer到它的orders自动的级联所有的插入、更新或删除操作。
8. 映射值对象Address,新建一个AddressMap类,继承自ComponentMap<Address>,代码如下:
public class AddressMap : ComponentMap<Address> { public AddressMap() { Map(x => x.Line1).Not.Nullable().Length(50); Map(x => x.Line2).Length(50); Map(x => x.ZipCode).Not.Nullable().Length(10); Map(x => x.City).Not.Nullable().Length(50); Map(x => x.State).Not.Nullable().Length(50); } }
9. 映射Order实体,新建一个OrderMap类,继承自ClassMap<Order>,代码如下:
public class OrderMap : ClassMap<Order> { public OrderMap() { Id(x => x.ID).GeneratedBy.HiLo("100"); Map(x => x.OrderDate).Not.Nullable(); Map(x => x.OrderTotal).Not.Nullable(); References(x => x.Customer).Not.Nullable(); References(x => x.Employee).Not.Nullable(); HasMany(x => x.LineItems).Inverse().Cascade.AllDeleteOrphan(); } }
10. 映射LineItem实体,新建一个LineItem类,继承自ClassMap<LineItem>,代码如下:
public class LineItemMap : ClassMap<LineItem> { public LineItemMap() { Id(x => x.ID).GeneratedBy.HiLo("100"); References(x => x.Order).Not.Nullable(); References(x => x.Product).Not.Nullable(); Map(x => x.Quantity).Not.Nullable(); Map(x => x.UnitPrice).Not.Nullable(); Map(x => x.Discount).Not.Nullable(); } }
11. 最后,映射Product实体,新建一个ProductMap类,继承自ClassMap<Product>,代码如下:
public class ProductMap : ClassMap<Product> { public ProductMap() { Id(x => x.ID).GeneratedBy.HiLo("100"); Map(x => x.Name).Not.Nullable().Length(50); Map(x => x.Description).Length(4000); Map(x => x.UnitPrice).Not.Nullable(); Map(x => x.ReorderLevel).Not.Nullable(); Map(x => x.Discontinued); } }
至此,我们完成了模型的映射。现在NHibernate可以理解如何在数据库中组织来自domain的数据了。