第三章(上) 让我们告诉NHibernate数据库结构

你一定很兴奋开始你的NHibernate学习之旅。我们正准备这么做。在本章中,我们将讨论为域模型声明映射。你声明映射来告诉NHibernate你的域模型中的类如何与数据库表匹配。如果您已经构建了适合域模型的数据库,那么映射看起来会非常简单。但是,如果数据库和领域模型彼此不兼容,那么映射就会变得复杂起来。对映射有全面的了解会在这种情况下有所帮助。声明映射的方法有多种,从基于xml的映射到基于代码的映射。我们将介绍三种最广泛使用的方法,即XML映射代码映射和 fluent mappings连贯映射

 

映射是NHibernate中最重要的概念。NHibernate与数据库交互的方式很大程度上依赖于映射的编写。在本章中,我们将介绍映射的基础知识。当我们在接下来的章节深入到另一个世界,我们将会重新审视映射,看看一个映射的细微调整在NHibernate与你的数据库交互时是如何带来巨大的改变的。这一章会让你对如何声明映射有一个基本的了解,并让你准备好投入到更高级的NHibernate主题中去。

 

1.NHibernate重要术语

从这一章开始,我们将使用一些术语。这些术语在NHibernate或应用程序的数据访问层中有特殊的含义。了解这些术语的含义,以及我们在本章和本书其余部分将如何使用它们,将会有所帮助。你可能已经知道其中一些术语。

POCO, Plain Old CLR Object 在C#中对类的另一种称呼

  • 不继承任何框架中定义的类
  • 不实现在任何框架中定义的接口
  • 不使用任何框架中定义的属性

 

在本书中,我们大部分时间只使用class这个词,但如果我在一些地方不得不使用POCO,您应该明白我的意思。

  • Entity:有标识符的类,至少在NHibernate维护的实体图中标识实例。这可能会让人有点困惑,但是随着我们在本章的学习,你会对NHibernate有更多的了解,从而更清楚地理解它。在此之前,每当我提到实体时,假设我引用的是类/POCO。
  • 持久化类:NHibernate能够使用定义的映射在一个或多个数据库表中持久化的类。
  • 特性/属性:当讨论POCO/类时,这意味着类的getter/setter属性或字段。当用于XML,这意味着XML属性。

 

2.映射的先决条件

类,正如我们在前一章中指出的,在NHibernate中不能直接使用。在NHibernate将类持久化到数据库之前,每个类都必须满足一些先决条件。持久化类必须至少满足以下两个要求:

  • virtual:持久化类每个属性应该定义为public virtual,   protected virtual  protected internal virtual. 三者之一。这是为了延迟加载。延迟加载是一个重要的特性。我们将在<第6章 让我们从数据库中检索一些数据>详细讨论,在那里我们将学习从数据库中检索实体。现在,值得一提的是,延迟加载只对持久化类的虚属性有效。说到这里,让我也告诉你NHibernate也能处理私有属性,如果需要的话,我们会研究的。但是在大多数情况下,除非使用遗留数据库,否则不需要声明私有属性。为了使用延迟加载,实体上的每个非私有方法也必须声明为virtual。
  • 标识:持久化类都必须有一个标识属性。标识用于区分同一个类的两个实例。标识映射到数据库表的主键。传统上,数据库自动生成标识/主键值。但这种方法可能存在一些陷阱。另一方面,NHibernate能够使用多种不同的策略生成标识。我们将在本章的标识生成部分详细研究这些内容。

 

第一个先决条件非常简单。我们只需要将virtual关键字添加到域模型中的所有属性和方法中。对于第二个先决条件,我们有多种选择,但我现在想简单处理。我们将向域模型中的所有实体添加一个integer类型的identifer属性。由于这是一个出现在每个域类上的属性,我们可以将其重构为一个公共基类。这个类看起来是这样的:

1 public abstract class EntityBase
2 {
3     public virtual int Id { get; set; }
4 }

注意,我们将这个类标记为抽象。该类的目的只是为域实体提供一个公共基类。下面是我们的Employee类的更改:

 1 public class Employee : EntityBase
 2 {
 3     public virtual string EmployeeNumber { get; set; }
 4     public virtual string Firstname { get; set; }
 5     public virtual string Lastname { get; set; }
 6     public virtual string EmailAddress { get; set; }
 7     public virtual DateTime DateOfBirth { get; set; }
 8     public virtual DateTime DateOfJoining { get; set; }
 9     public virtual Address ResidentialAddress { get; set; }
10     public virtual bool IsAdmin { get; set; }
11     public virtual string Password { get; set; }
12     public virtual ICollection<Benefit> Benefits { get; set; }
13 }

为了简单起见,我在这里省略了其他类,但我希望您能理解问题的关键。通过前面的代码更改,我们现在可以为域模型编写映射了。我们先快速介绍编写映射的不同机制(NHibernate提供的机制)。

 

3.编写映射的不同机制

NHibernate至少有三种不同的机制来映射持久化类到数据库表。这既是好事,也是坏事。虽然您可以选择最舒服的选项,但这也会让您有时感到困惑。其中两种机制是NHibernate库本地支持的,另一种是外部实现的。在这一节中,我们将在继续为我们的员工福利问题实现映射之前,简要地看看这三种机制。尽管有三种不同的方式来表示实体映射,但请记住,无论哪种方式表示映射,映射引擎的核心都是相同的。对于本章中的所有映射讨论,我计划在大多数情况下使用所有这三种方法来表示映射。通过这种方式,您将对每种方法的表现力有一种感觉。只使用一种方法而忽略其他方法不会有什么坏处,但是要记住,XML映射是最完整的,了解它们在一些棘手的情况下总是有帮助的。

 

3.1. XML 映射

使用XML文件将持久化类映射到数据库是最古老、最原始的映射方法。这是Hibernate首先支持的。NHibernate就是这么移植的。在此方法中,您需要使用NHibernate提供的元素和属性写XML映射文件。

正如我们将在本章和接下来的章节中看到的,映射会随着业务需求的复杂性增长而变得非常详细和复杂。XML映射的最大优点是你可以为NHibernate支持的每个特性指定映射。另外两种机制在支持NHibernate的每个特性方面都有一些限制。例如,在XML映射中,受保护属性的映射与公共属性的映射没有什么不同。对于另外两种机制,情况就不一样了。可以将XML映射看作是所有映射特性的超集。

 

虽然使用XML映射有一些明显的优点,但也有一些缺点。最令人不满的是,XML映射对重构并不友好。正如我们将在本章看到的,XML映射使用了很多奇怪的字符串来引用类和属性。当您重命名一个类或属性而忘记更新XML映射时,这可能会导致一些细微的缺陷。我个人认为,如果您编写的单元测试很好,并且每次更改代码时都会警告失败代码,那么这并不是什么大问题。您仍然需要手动更新XML条目,但至少可以避免提交后出现功能崩溃的尴尬。

 

3.2.Fluent 映射
XML是它那个时代的英雄,但很快它的名声就在减弱,开发社区开始关注所有非XML选项。NHibernate仍然只支持XML映射。一些热心的开发人员决定构建一种新的映射持久类的机制。他们构建了一个库叫做Fluent NHibernate(FNH).请允许我引用FNH的网站(http://www.
fluentnhibernate.org/
)。他们是这么说的:

“流畅、无xml、编译安全、自动、基于约定的NHibernate映射”。

使用FNH,您不需要编写一行XML。你用的只是他们的Fluent API。到目前为止,这是最著名的编写映射的非xml机制。

通过使用lambda表达式,FNH设法避免了使用奇怪字符串来引用类和属性。但这增加了约束,因为lambdas只能访问公共属性。如果您想映射受保护/私有属性,那么FNH提供了一些方法,但是没有一种方法是如此优雅。关于FNH需要注意的另一件事是他们对命名的选择。FNH的开发人员选择使用不同于XML中用于表示映射的名称。例如要映射属性,XML使用标签属性而FNH使用map。这可能会在一开始造成混淆,如果你是从FNH到XML,反之亦然。

 

3.3.代码映射

FNH变得如此著名,以至于NHibernate的作者们感到有必要在NH中本地支持FNH。但是FNH API明显偏离了XML约定这一事实。NHibernate团队想要一个更接近XML约定的API。另一个名为“ConfORM“的开源库就能实现这一点,NHibernate的作者从遵从的风格中获得了灵感,建立了对代码映射的支持。3.2版本是第一个有这种支持的发布版本,但是实现还没有完成,因此它从来没有像其他方法那样得到广泛的采用。NHibernate的最新版本在代码映射方面有很多修复,这使得它非常稳定。

 

代码映射的API,符合命名约定和XML映射结构。用于声明映射的方法名称与相应的XML元素名称匹配所有API方法基本上都接受两个参数(有几个重载来适应边界情况)。第一个是被映射的属性,第二个是用于声明附加映射信息的lambda。这与声明XML映射的方式非常相似。

 

我必须说,与FNH相比,这不是更好的API,因为它非常类似于XML,而且如果不彻底理解XML映射,就不能编写映射代码。通过代码进行映射的一个好处是,它允许您通过override重写和一些lambda来映射私有属性或字段。虽然代码映射没FNH API用着爽,但它总比XML好多了,因为XML映射对重构不友好。

我们将使用所有这三种方法来映射我们的员工福利域。我们首先从XML映射开始,深入了解如何为不同的场景编写映射。然后,我们将使用另外两种技术重写相同的映射。

 

 

4.Employee类的XML映射

对于初学者来说,通过文档学习映射可能不是最好的体验。最好拿我们领域模型中的一个类作为示例,指导您完成映射练习。这正是我们这一节要做的。我们将从为Employee类编写XML映射开始我们的旅程。

Employee类有一些基本类型的属性以及与其他类的集合和关联。为了在开始时保持简单,我们将首先查看Employee类的简单属性的映射,而不是与其他类的关联属性。目标是在开始处理复杂场景之前理解如何声明简单映射。

在我们开始映射之前,我想谈谈如何为XML映射和NHibernate的开发环境做好准备。我的意思是创建工程,第三方库安装,以及任何Visual Studio配置完成。我们将使用单元测试来验证映射。还将讨论编写和运行单元测试所需的VS配置。

 

4.1 准备好开发环境

在我们的Visual Studio解决方案中,我们已经有一个名为Domain的项目,它包含所有的域实体。让我们创建另一个名为Persistence的类库工程。我们将在这个工程中添加XML映射。按照下面列出的步骤设置您自己的XML映射之旅:

  1. 在Persistence工程中,添加对Domain项目的引用。 
  2. 将NuGet包NHibernate添加到Persistence项目中。
  3. 将名为Mappings的文件夹添加到新创建的Persistence项目中。
  4. 在新添加的Mappings文件夹下添加名为Xml的文件夹。我们将把XML映射添加到这个文件夹中。

每个XML映射文件必须以.hbm.xml结尾。为了简单性和可读性,许多开发人员都遵循一个惯例,将它们命名为<Entity name>.hbm.xml。例如,Employee类的XML映射fle将命名为Employee.hbm. XML。但这不是必须的,您可以随意地为映射程序命名,只要它们以.hbm.xml结尾。

 

4.2 获得处理XML映射程序的智能感知

Visual Studio最强大的功能之一是它的智能感知功能。作为一个长期使用Visual Studio(和ReSharper)的用户,我发现当IntelliSense在一些非标准的文本文件上不起作用时,我感到很不舒服。对于XML映射来说,幸运的是NH的作者提供了支持智能感知的XSD文件。按照下面列出的步骤为XML映射程序启用智能感知。对于这些说明,我假设在您的项目中添加了一个名为Employee.hbm.xml的XML文件。

  1. 在解决方案资源管理器中打开Employee.hbm.xml。
  2. 打开后,右键单击文档的任何地方。
  3. 在右键菜单中,选择Properties选项。
  4. 这将打开Xml文档的属性对话框,如下图所示。单击Schemas属性,一个省略号按钮就会显示出来:

 

5.单击省略号按钮,将弹出XML Schemas对话框,如下面的截图所示:

 

6. 点击Add…按钮将打开一个浏览对话框。 

7. 浏览到NuGet包的下载位置。这通常在解决方案文件夹级别的名为packages的文件夹中。这里你可以找到两个XSD文件,名为nhibernate-configuration.xsd 和 nhibernate-mapping.xsd。这两个都选上。

8. 在XML Schemas对话框上单击OK。

9. 祝贺您,您已经成功地为XML映射文件启用了智能感知。

这样,您的开发环境就可以使用XML映射了。因此,让我们从映射Employee类开始。

 

4.3 单元测试验证Employee映射

为了验证映射是否正确,以及它们是否按照我们的期望映射到数据库表,我们将创建Employee类的实例并将其保存到内存中的数据库。然后,我们将检索刚刚保存的实例,并确认已保存的所有属性值都是从数据库中检索的。向Tests.Unit工程添加一个新类,命名为InMemoryDatabaseForXmlMappings如下代码:

 1 namespace Tests.Unit
 2 {
 3     public class InMemoryDatabaseForXmlMappings : IDisposable
 4     {
 5         protected Configuration config;
 6         protected ISessionFactory sessionFactory;
 7         
 8         public InMemoryDatabaseForXmlMappings()
 9         {
10             config = new Configuration()
11                     .SetProperty(Environment.ReleaseConnections, "on_close")
12                     .SetProperty(Environment.Dialect, typeof(SQLiteDialect).AssemblyQualifiedName)
13                     .SetProperty(Environment.ConnectionDriver, typeof(SQLite20Driver).AssemblyQualifiedName)
14                     .SetProperty(Environment.ConnectionString, "datasource=:memory:")
15                     .AddFile("Mappings/Xml/Employee.hbm.xml");
16             
17             sessionFactory = config.BuildSessionFactory();
18             
19             Session = sessionFactory.OpenSession();
20             
21             new SchemaExport(config).Execute(true, true, false, Session.Connection, Console.Out);
22         }
23         
24         public ISession Session { get; set; }
25         
26         public void Dispose()
27         {
28             Session.Dispose();
29             SessionFactory.Dispose();
30         }
31     }
32 }

 

正如你所看到的,这里发生了很多事情。让我对前面的代码做一个简短的解释。在类的构造函数中,我们创建了NH Configuration对象的实例,并设置了不同的配置值。这些属性告诉NHibernate我们将使用什么样的数据库以及NHibernate可以在哪里找到这个数据库。需要注意的一点是,我们使用的是SQLite的内存数据库。

然后AddFile将Employee.hbm.xml文件名添加到配置,让NHibernate知道去哪个文件找Employee实体的映射。这是配置映射的一种方式。NHibernate支持几种不同的方式,我们将在下一章详细介绍。然后创建session对象的实例。如果你还记得第1章NHibernate介绍,会话对象是让我们连接到数据库。最后一行使用SchemaExport类,为confguration配置提供映射,并将数据库结构(我们在各个映射中定义的)创建到内存数据库中(就是Code first)。你可以暂时不纠结看不懂的细节,只要记住这个类允许我们使用内存数据库来进行测试。在下一章中,我们将详细介绍NHibernate配置。

NH配置搞定后,来看我们的第一个单元测试。因为我们只映射Employee类上的简单属性,所以先验证Employee类的简单属性是否被正确地保存在数据库中。在项目Test.Unit中添加一个名为Mappings的新文件夹。在该文件夹中,添加一个名为EmployeeMappingsTests的新类。再添加一个名为MapsPrimitiveProperties的方法到这个类,如下所示:

 

 1 using System;
 2 using Domain;
 3 using NHibernate;
 4 using NUnit.Framework;
 5 
 6 namespace Tests.Unit.Mappings.Xml
 7 {
 8     [TestFixture]
 9     public class EmployeeMappingsTests
10     {
11         private InMemoryDatabaseForXmlMappings database;
12         private ISession session;
13         
14         [TestFixtureSetUp]
15         public void Setup()
16         {
17             database = new InMemoryDatabaseForXmlMappings();
18             session = database.Session;
19         }
20 [Test] 21 public void MapsPrimitiveProperties() 22 { 23 object id = 0; 24 using (var transaction = session.BeginTransaction()) 25 { 26 id = session.Save(new Employee 27 { 28 EmployeeNumber = "5987123", 29 Firstname = "Hillary", 30 Lastname = "Gamble", 31 EmailAddress = "hillary.gamble@corporate.com", 32 DateOfBirth = new DateTime(1980, 4, 23), 33 DateOfJoining = new DateTime(2010, 7, 12), 34 IsAdmin = true, 35 Password = "Password" 36 }); 37 38 transaction.Commit(); 39 } 40 41 session.Clear(); 42 43 using (var transaction = session.BeginTransaction()) 44 { 45 var employee = session.Get<Employee>(id); 46 47 Assert.That(employee.EmployeeNumber, Is.EqualTo("5987123")); 48 Assert.That(employee.Firstname, Is.EqualTo("Hillary")); 49 Assert.That(employee.Lastname, Is.EqualTo("Gamble")); 50 Assert.That(employee.EmailAddress, Is.EqualTo("hillary.gamble@corporate.com")); 51 Assert.That(employee.DateOfBirth.Year, Is.EqualTo(1980)); 52 Assert.That(employee.DateOfBirth.Month, Is.EqualTo(4)); 53 Assert.That(employee.DateOfBirth.Day, Is.EqualTo(23)); 54 Assert.That(employee.DateOfJoining.Year, Is.EqualTo(2010)); 55 Assert.That(employee.DateOfJoining.Month, Is.EqualTo(7)); 56 Assert.That(employee.DateOfJoining.Day, Is.EqualTo(12)); 57 Assert.That(employee.IsAdmin, Is.True); 58 Assert.That(employee.Password, Is.EqualTo("Password")); 59 60 transaction.Commit(); 61 } 62 } 63 } 64 }

 

小贴士:

如果你细心看,我们添加了一个[TestFixtureSetup]特性到Setup方法上。该特性告诉NUnit在执行该类的测试方法之前,应该先执行此被标注的Setup方法一次(预先初始化)。类似地,还有一个[TextFixtureTearDown]特性。在所有测试结束后,NUnit调用被标注的方法(善后清理)。

 

 

我们有一个名为Setup的方法,在其中创建一个实例获取会话对象。我们使用这个会话来存储创建的Employee对象实例。暂且不提对BeginTransaction方法的调用。保存employee实例之后,清除会话。然后,我们使用同一个会话对象来加载我们刚刚保存的employee记录,验证读到的和前面存入的记录属性值是否一致。

 

 小贴士:

我倾向于在一个类中添加不超过4-5个单元测试。因此,我们在本章中提到的单元测试可能并不都在同一个类中。但是所有单元测试类之间有一个共同点。在每个单元测试中,Setup方法及其代码都是完全相同的。如果需要,可以将其移到公共基类中。还要注意,今后,我将只向您展示单元测试,而不展示Setup方法或任何其他细节。这样做是为了使章节的正文保持简短。

 

如果您现在运行这个测试,它将失败,原因很明显。我们还没有添加任何XML映射文件。让我们继续,开始为上面测试的属性编写映射。

 

4.4 映射

在上一节中,我们已经看到XML映射是用一个扩展名为.hbm.xml的文件编写的。对于Employee类,这将是Employee.hbm.xml文件。下面是如何映射Employee类的简单属性:

 

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Domain" namespace="Domain">
 3 <class name="Employee">
 4     <id name="Id" generator="hilo" />
 5     <property name="EmployeeNumber" />
 6     <property name="Firstname" />
 7     <property name="Lastname" />
 8     <property name="EmailAddress" />
 9     <property name="DateOfBirth" />
10     <property name="DateOfJoining" />
11     <property name="IsAdmin" />
12     <property name="Password" />
13 </class>

让我们试着理解前面代码中不同XML元素的目的:

  • hibernate-mapping:该元素是XML映射文件的根元素,保存子元素中的所有映射信息。除了它的标准XML名称空间属性之外,这个元素还有两个重要的属性。第一个是assembly,用于声明类Employee的程序集名称第二个属性是namespace,它用于声明类的完整名称空间。在我们的例子中,两者都是Domain。
  • class:这个元素用于声明编写映射的类名称。属性name显然用于声明类的名称。
  • id:该元素用于指定此实体的标识符属性的详细信息。属性name指定类上保存标识的属性名。属性生成器告诉NHibernate应该如何生成标识值。我们在本章开始的映射条件一节中简要讨论了这一点。generator 的值是NHibernate捆绑的几种标识生成算法之一的缩写(hilo是缩写)。
  • property:这个元素用于映射简单属性。属性name指定映射类的哪个属性。

如果我们现在运行我们的测试,它应该会愉快地通过。

我们在上面看到的是NHibernate需要的最小配置。但是我们前面讨论的每个元素都有更多的可用属性。这些属性让我们可以非常详细地控制映射。当这些属性没有被声明时,NHibernate使用安全的默认值。让我们看看每个元素的一些重要和最有用的可选属性。

 

4.4.1 Hibernate-mapping

前面示例的hibernate映射详细声明如下:

1 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
2     schema="EB"
3     default-cascade="delete-all-orphan"
4     assembly="Domain"
5     namespace="Domain"
6     default-access="property"
7     default-lazy="true">

让我们看看这些在XML映射文件中的全局属性给了我们什么:

  • schema :通常,我们使用database schema 数据库架构将表划分为逻辑组。如果是这样,你必须告诉NHibernate。但是如果你把所有的表都放在同一个schema 中,你就不会想在每个类重复这个信息了。NHibernate允许你在hibernate映射级别全局声明这个。如果没有声明,NHibernate针对MS SQL Server会用dbo作默认值。
  • default-cascade: 这允许您为整个应用程序指定默认的级联样式。如果未指定,则默认none。

 

小贴士:

cascade 级联定义了通过外键相互关联的表的保存、删除和更新的工作方式。当父表中的记录被删除时,您可以将子表中的记录配置为自动删除或保留为孤儿,等等。类似地,当一个父实体被保存时,相关的实体也会被NHibernate自动保存。

 

  • default-access: 我们已经在刚才看到的映射中对Employee类进行了映射属性。但是NHibernate并不仅限于映射属性。它还可以映射字段。通过这个属性,你可以告诉NHibernate你想映射字段而不是属性。同样,如果未指定,NHibernate默认映射属性。
  • default-lazy: 如果该属性设置为true,则将启动整个应用程序的延迟加载。我们在本章的开头简要地讨论了延迟加载。我们将在后面的章节中详细介绍它。NHibernate默认这个设置为true。

 

小贴士:

只有在将所有映射保存在一个hibernate映射节点下的一个XML文件中时,这些属性的全局作用域才会生效。我个人不喜欢这种风格,并倾向于将相关类的映射保存在一个类中在这种情况下,我需要为每个文件添加一次这些属性。

 

4.4.2 Class

示例中类元素的详细映射声明如下: 

1 <class name="Employee"
2     table="employees"
3     mutable="true"
4     schema="EB"
5     lazy="true">

让我们来看看这些附加属性,了解它们提供了哪些附加特性:

  • table:此属性声明类应该映射到的表的名称。如果没有声明,NHibernate使用一个叫做命名策略的概念来确定实体被映射到的表的名称。通过实现接口NHibernate.Cfg.INamingStrategy来实现命名策略。有两种已有的实现。首先,DefaultNamingStrategy使用实体名作为表名。顾名思义,如果在映射过程中没有明确指定表名,这是NHibernate会使用的默认命名策略。 第二个实现是ImprovedNamingStrategy,它使用下划线将Pascal大小写段从实体的名称中分离出来,以形成表的名称。例如,名为SeasonTicketLoan的实体将被映射到名为season_ticket_loan的数据库表。

  • mutable: 这个属性指定这个类的实例是否应该是可变的。默认情况下,NHibernate默认它为true,但是你可以通过设置这个值为false来使实例不可变。(比如值类型的不可变性)
  • schema:这与我们前面讨论的schema属性相同,但是作用域仅限于被映射的类
  • lazy:这类似于前面讨论的default-lazy属性,其作用域仅限于被映射的类。

 

4.4.3 Property
指定重要的可选属性的属性映射如下:

1 <property
2     name="EmployeeNumber"
3     column="employment_number"
4     type="System.String"
5     length="10"
6     not-null="true"
7     unique="true"
8     lazy="false"
9     mutable="true"/>

再一次,让我们来看看这些属性的含义:

  • column: 用于指定映射数据库表列的名称。如果没有定义,NHibernate使用命名策略来确定列名。这与我们在前面讨论类映射的表属性时提到的命名策略相同。
  • type:映射数据库列的数据类型。NHibernate有一个精心设计的系统,它使用反射来检查被映射的属性的类型,并为映射的表列确定最合适的数据库类型。如果不满意NHibernate为列指定的数据类型,你可以重写它并使用这个属性指定你自己的类型。NHibernate接受一些基本的.net类型,也提供了更多的类型。在附录中,你可以找到描述.net / nhibernate类型映射到哪种数据库类型的表格。
  • length:NHibernate可以根据你的映射生成数据库脚本。我们将在其中一章中看到NHibernate的这个特性。NHibernate使用你在映射中指定的信息来创建一个精确映射到你的域模型的数据库模式。为此,长度属性可用于映射到支持长度的数据库类型的属性,例如char。这个属性是可选的,如果没有指定,默认值为255。
  •  not-null :此属性用于类似于前一个属性的情况,并声明映射表列为NOT NULL。
  •  unique :这是同一类别中的另一个属性。此属性将映射表列标记为唯一的。
  •  lazy:这类似于类上的lazy属性,不同的是作用域仅限于被映射的属性。这只对CLOB和BLOB类型属性有用。
  •  mutable:这与类元素上的可变属性相同,唯一的区别是范围仅限于当前属性

 

小贴士:

这里我有意跳过了一些重要的可选属性。这些属性控制NHibernate如何为属性存储/检索数据。我打算在<第5章 让我们存储一些数据到数据库中>详细讨论这些,我们将在那里看到存储和检索数据。

 

到目前为止,我们只是触及了NHibernate映射的皮毛。要详细解释这个主题,书中的一个章节是不够的。在这一部分,我想给你一个NHibernate映射的一瞥。现在你已经了解了什么是NHibernate映射,如何声明它们,以及它们是如何工作的,我想介绍一些关于高级映射场景的细节,主要是关于映射关联、组件和继承映射策略

 

5.标识生成器

我们已经看到,在映射标识时指定了生成器(缩写)。生成器属性用于声明用于生成标识值的算法。在generator 属性中使用的值要么是NHibernate提供的开箱即用的算法的短名,要么是实现该算法的NHibernate类的程序集限定名。NHibernate已经实现了几个这样的算法,可以应对任何身份生成要求。但如果您不满意这些,那么您可以建立自己的算法实现接口NHibernate.Id.IIdentifierGenerator.

让我们来看看NHibernate提供的开箱即用的标识生成算法。

5.1 Identity

该算法在SQL Server和MySQL支持的标识列后面工作NH将在不分配任何标识值的情况下保存实体,而数据库将在保存实际数据库记录之前生成标识值。NHibernate然后检索由数据库生成的标识,Id返回到应用程序代码。

 

5.2 Sequence

与identity相似,该算法与所支持的序列一起工作Oracle、PostgreSQL和SQL Server 2014。

 

5.3 Hilo
这个实现使用hi/lo算法来生成标识。算法使用从数据库中检索到的高值,并将其与低值范围相结合,生成唯一的标识。默认情况下,从表格hibernate_unique_key的next_hi列中获取高值。但是您可以重写它以使用另一个表。该算法还支持指定一个where参数,用于从hibernate_ unique_key表的不同行中检索不同实体的高值。在<第5章 我们将在数据库中存储一些数据>中我们将更详细地讨论hilo算法。
 

5.4 Seqhilo

这个实现类似于hilo。唯一的区别是,它使用一个命名的数据库序列作为hi值的源。这种策略显然只适用于支持序列的数据库

 

5.5 GUID

如果类的标识属性是System.Guid。那么您可以使用全局唯一标识(Guid)生成器来生成标识值。该算法生成了一个新的System.Guid。为插入到数据库中的每个记录设置Guid。

 

5.6 Guid.comb

这是对System.Guid类型标识的生成器实现的改进。在大多数关系数据库中,主键(NHibernate中的标识)被聚集并自动建立索引。System.Guid值不是索引友好的。因此,Jimmy Nilsson提出了一种生成System.Guid的不同机制。索引友好且性能更好的Guid值。生成器实现了Jimmy提出的算法。你可以在这个链接查看详细内容:http://www.informit.com/articles/article.asp? 

 

5.7 Native  

该生成器没有自己的算法。它根据底层数据库的功能选择身份、序列或hilo中的一个

 

5.8 Assigned

与native类似,assigned没有任何算法来生成identifer值。它是用来让NHibernate知道一个应用程序会在实体被保存到数据库之前分配标识值。

 

我们在这里只讨论了最重要和广泛使用的算法。请注意,还有更多现成的工具。其中hilo是最著名的,也是最高效的数据库操作的结果。我们将在<第5章 我们将一些数据存储到数据库中>讨论细节。现在,让我们继续下一个话题。

 

6.映射关联

两个类之间的关联是一个非常容易理解的概念。如果你想在ClassA和ClassB之间有关联,那么你可以在ClassA上添加一个ClassB类型的属性。这是最基本的关联形式。

关联有四种不同的形式,如下所述:

  • One-to-many: 一对多,关联的一端只有一个实体实例,而另一端有多个实例。在代码中,这由集合类型的属性表示。例如,IList<T>, [],ICollection < T >, IEnumerable < T >。等等
  • Many-to-one: 多对一,一对多的反义词。
  • One-to-one: 一对一,关联的两端都只有一个实体实例。
  • Many-to-many: 多对多,关联的两端都有多个实体实例。

在我们的Domain中,除了多对多之外,我们已经有了所有关联的示例。以下是部分员工类和福利类:

 

 1 public class Employee : EntityBase
 2 {
 3     public virtual ICollection<Benefit> Benefits { get; set; }
 4     public virtual Address ResidentialAddress { get; set; }
 5 }
 6 public class Benefit : EntityBase
 7 {
 8     public virtual Employee Employee { get; set; }
 9 }
10 public class Address : EntityBase
11 {
12     public virtual Employee Employee { get; set; }
13 }

Employee类上的Benefits属性就是一对多关联的一个例子。在这个关联的一边,我们得到了Employee类的一个实例,而在另一边,我们得到了Benefit类的一个实例列表。

 

Benefit 福利类上,我们把福利和员工联系起来。这是我们刚刚讨论的一对多关联的另一端。这显然是多对一关联。我们可以让Benefit类的多个实例属于同一个Employee实例。另一方面,Employee类上的ResidentialAddress属性是一对一关联的一个示例。每个员工只有一个地址。同样,从Address返回到Employee的关联也是一对一的。

 

我们的Domain中没有任何多对多的例子,所以我将扩展问题的范围,并添加以下新的业务需求:

公司管理若干社团。员工被鼓励成为不同社团的成员。这些社团不仅能帮助建立一个社交圈,还能给员工提供机会,让他们认识志同道合的人,并通过分享经验来提高自己的技能。员工可以同时成为多个社团的成员。

为了满足上述需求,我们需要在域中添加一个新类,以表示一个雇员社团社团可以有多个成员,员工也可以加入多个社团。以下是代码部分:

 

 1 namespace Domain
 2 {
 3     public class Community : EntityBase
 4     {
 5         public virtual string Name { get; set; }
 6         public virtual string Description { get; set; }
 7         public virtual ICollection<Employee> Members { get; set; }
 8     }
 9 }
10 namespace Domain
11 {
12     public class Employee : EntityBase
13     {
14         public virtual ICollection<Community> Communities { get; set; }
15     }
16 }

因此,我们在Employee上有一个Community类的集合,它表示该员工所属的社区。同时,我们在Community上有一个Employee类的集合,其中包含该Community的所有成员。

 

6.1.数据库表中的关联

在我们真正研究映射不同类型的关联之前,有必要花些时间了解关系数据库是如何支持关联的。我们知道域模型中的每个实体都被映射到一个数据库表。因此,为了支持两个类之间的关联,数据库必须能够以某种方式关联两个数据库表。关系数据库通过两个表之间的外键关系来实现这一点。如果我可以使用Employee和Benefit类之间的关联作为例子,那么下面的数据库图解释了这两个实体在数据库中是如何相互关联的:

 

 

可以看到,福利表上的Employee_Id列被指定为Employee表的外键。此列中保存的值是Employee表中记录的主键/标识。您可以在任意多个利益记录中重复相同的主键。这意味着对于其中的一个记录Employee表,您可以在Benefit表中存储许多记录。这是一对多关系的基础。

 

基本上,数据库只支持使用共享主键的一对多关系和一对一关系。我们前面讨论的所有其他关系只是一对多关系的不同安排,以实现不同的结果。例如,通过为Employee表中的一条记录在Benefit表中插入一条以上的记录这一逻辑约束,可以实现一对一的关系。类似地,您可以通过一个中间表组合两个一对多关系来实现多对多关系。

接下来,我们将研究如何将类之间的关联映射到数据库关系。与前面的映射练习一样,我们将使用单元测试来确认我们的映射达到了我们想要的效果。 

 

6.2.一对多的关联

Employee类上的Benefits集合是一对多的关联,我们在这里映射它。我们将使用以下单元测试来驱动映射的实现。在这个测试中,我们添加了一个Leave的实例,将SeasonTicketLoan和SkillsEnhancementAllowance类存储到Employee类的福利集合中,并将Employee实例存储在数据库中。然后检索实例,并确认所有的福利实例都已存在:

 

 1 [Test]
 2 public void MapsBenefits()
 3 {
 4        object id = 0;
 5        using (var transaction = session.BeginTransaction())
 6         {
 7             id = session.Save(new Employee
 8             {
 9                 EmployeeNumber = "123456789",
10 Benefits = new HashSet<Benefit> 11 { 12 new SkillsEnhancementAllowance 13 { 14 Entitlement = 1000, 15 RemainingEntitlement = 250 16 }, 17 18 new SeasonTicketLoan 19 { 20 Amount = 1416, 21 MonthlyInstalment = 118, 22 StartDate = new DateTime(2014, 4, 25), 23 EndDate = new DateTime(2015, 3, 25) 24 }, 25 26 new Leave 27 { 28 AvailableEntitlement = 30, 29 RemainingEntitlement = 15, 30 Type = LeaveType.Paid 31 } 32 } 33 }); 34 transaction.Commit(); 35 } 36 37 session.Clear(); 38 39 using (var transaction = session.BeginTransaction()) 40 { 41 var employee = session.Get<Employee>(id); 42 Assert.That(employee.Benefits.Count, Is.EqualTo(3)); 43 transaction.Commit(); 44 } 45 }

这个测试和我们之前看到的测试没有很大的不同。在从数据库中检索保存的employee实例之后,我们发现它上的Benefits集合有三个条目,这是我们添加的。让我们添加映射来通过这个测试。这个关联的映射将被添加到Employee类的现有映射文件中,即Employee.hbm.xml。下面是一对多关联映射的声明:

 

1 <set name="Benefits" cascade="all-delete-orphan">
2     <key column="Employee_Id" />
3     <one-to-many class="Benefit"/>
4 </set>

 

一对多映射在NHibernate中也称为集合映射。这是因为映射的属性通常表示一组项。需要在set元素上声明的必须属性是要映射的属性的名称

在set节点中有两个嵌套的元素。第一个是key,它用于声明关联另一端的外键列的名称。如果您还记得前面的图,Employee类的Benefit的外键名为Employee_Id。

该集合中的第二个节点是one-to-many 一对多的。在这里,我们将更多地介绍这个关联的各个方面。惟一必需的详细信息是另一端的类的名称。

您可能已经注意到,我还在上面的映射上添加了一个级联 cascade属性。这是一个个人偏好,你可以很容易地去掉它,但是要记住NHibernate默认的值是none。

我相信“一图胜千言”。在下面的图片中,我试图解释如何映射数据库表和域类:

 

 

6.2.1.集合的类型

我们使用了set元素来声明集合映射。Set告诉NHibernate被映射的属性是一个集合。此外,set还设置了集合无序、未索引和不允许重复的限制。您可以使用以下XML元素将集合声明为有序、索引等:

  • Bag
  • List
  • Map
  • Array

虽然它们都将集合属性映射到数据库,但在它们的运行时行为和它们所支持的. net类型方面存在细微的差异。下表给出了这些映射的键比较:

 

 

了解上述关键差异,再加上每个集合映射提供的其他一些特性,对于确定哪个集合映射最适合完成工作非常有用。我不打算在这里讨论这些细节,因为目的只是介绍集合映射。在接下来的章节中,我们将详细介绍集合映射,并研究其中的一些特性。

 

小贴士:

所有关联映射都有相当详细的级别配置。我宁愿只向您展示使关联工作所需的最低限度。一些可选的混淆将在本书适当的时候被涵盖。Rest用于罕见和特殊的情况,留给读者自己探索。

 

6.3.多对一的关联

 一对多关联的另一端是多对一关联。当一对多和多对一关联同时存在于两个实体之间时,称为双向关联。这是因为您可以在两个方向上从一端导航到另一端。在我们的领域模型中,Benefit 到 Employee 就是多对一关联的一个例子。如果您注意到的话,这也是一个双向关联。之所以强调关联的双向本质,是因为NHibernate用不同的方式处理它们。我们将在第五章讨论,但是现在值得强调一下双向映射是什么。

 

为了测试这个映射,我们将从前面的集合映射示例中扩展单元测试,并在断言beneft为3的那一行后面添加以下代码:

 1         var seasonTicketLoan = employee.Benefits.OfType<SeasonTicketLoan>().FirstOrDefault();
 2             Assert.That(seasonTicketLoan, Is.Not.Null);
 3             if (seasonTicketLoan != null)
 4             {
 5                 Assert.That(seasonTicketLoan.Employee.EmployeeNumber,
 6                 Is.EqualTo("123456789"));
 7             }
 8             var skillsEnhancementAllowance = employee.Benefits.OfType<SkillsEnhancementAllowance>().FirstOrDefault();
 9             Assert.That(skillsEnhancementAllowance, Is.Not.Null);
10             if (skillsEnhancementAllowance != null)
11             {
12                 Assert.That(skillsEnhancementAllowance.Employee.EmployeeNumber, Is.EqualTo("123456789"));
13             }
14             var leave = employee.Benefits.OfType<Leave>().FirstOrDefault();
15             Assert.That(leave, Is.Not.Null);
16             if (leave != null)
17             {
18                 Assert.That(leave.Employee.EmployeeNumber,
19                 Is.EqualTo("123456789"));
20             }

乍一看,这段代码可能看起来很复杂,但实际上并不复杂。我们已经从数据库加载了employee实例。在这个实例中,我们发现受益人列表中有3个受益人实例。我们使用LINQ查询这个列表并检索每种类型的受益人实例。在每个受益人实例上,我们发现Employee实例出现了,它与我们保存到数据库的Employee实例相同。

 

与集合映射相比,这种关联的映射相对容易。下面是如何在Benefit类的mapping file中声明这个映射,即在文件benefits .hbm.xml:

 

1 <many-to-one name="Employee"class="Employee"column="Employee_Id"/>

many-to-one节点,表示这是一对多关联的many侧(即Benefit类)。该映射只有两个必须属性。第一个,name标识关联属性。第二,class指定类。

一图胜千言:

 

 

 

6.4.一对一关联

在一对一关联中,关联的两端都持有单个项。关系数据库中有两种一对一关联。第一种是一对多关联的变体,它使用从一个表到另一个表的外键。与一对多关联的不同之处在于,这个外键具有惟一的约束,因此在多个方面只能显示一条记录。第二个变体使用共享主键方法,其中关联表共享主键值。这里我们只讨论惟一的外键方法。共享主键方法留给读者作为练习。

 

我们将使用员工到地址的关联来解释。为了详细说明我们刚才讨论的数据库关系,让我向您展示Employee和Address表的数据库关系图,以及如何使用Address表中的外键Employee_Id将它们关联到Employee表。如图所示:

 

 

 我们将使用以下单元测试来验证前面关联的映射:

 1 [Test]
 2 public void MapsResidentialAddress()
 3 {
 4     object id = 0;
 5     using (var transaction = Session.BeginTransaction())
 6     {
 7         var residentialAddress = new Address
 8         {
 9             AddressLine1 = "Address line 1",
10             AddressLine2 = "Address line 2",
11             Postcode = "postcode",
12             City = "city",
13 
14             Country = "country"
15         };
16         var employee = new Employee
17         {
18             EmployeeNumber = "123456789",
19             ResidentialAddress = residentialAddress
20         };
21         residentialAddress.Employee = employee;
22         id = Session.Save(employee);
23         transaction.Commit();
24     }
25     Session.Clear();
26     using (var transaction = Session.BeginTransaction())
27     {
28         var employee = Session.Get<Employee>(id);
29         Assert.That(employee.ResidentialAddress.AddressLine1,
30         Is.EqualTo("Address line 1"));
31         Assert.That(employee.ResidentialAddress.AddressLine2,
32         Is.EqualTo("Address line 2"));
33         Assert.That(employee.ResidentialAddress.Postcode,
34         Is.EqualTo("postcode"));
35         Assert.That(employee.ResidentialAddress.City,
36         Is.EqualTo("city"));
37         Assert.That(employee.ResidentialAddress.Country,
38         Is.EqualTo("country"));
39         Assert.That(employee.ResidentialAddress.Employee.EmployeeNumber, Is
40         .EqualTo("123456789"));
41         transaction.Commit();
42     }
43 }

小贴士:

您可能已经注意到,我显式地设置了Employee的ResidentialAddress属性,和Address的Employee属性。这是一个双向的关联,NHibernate通常确保两端都被正确地持久化,即使一端在代码中被设置。但在一对一关联的情况下,这并不总是正确的。我们将在第5章中讨论这些细节。当我们谈到持久化实体时。我只是想强调一下,这个场景的单元测试的编写略有不同。

 

因此,我们有一个从雇员到地址的一对一关联,还有一个从地址到雇员的一对一关联。从员工到地址的关联映射如下:

1 <one-to-one name = "ResidentialAddress" class = "Address" property-ref = "Employee" cascade = "all" />

到目前为止,您可能已经猜到了one-to-one 节点和name属性、class和 cascade 用于什么。唯一的附加属性property-ref用于在关联的另一端声明指向此实体的属性的名称。在我们的示例中,这将是Address类上名为Employee的属性。

从地址到员工的关联实际上是多对一的关联,仅限于单个项。它使用我们前面看到的多对一XML节点进行映射,使用一个指定惟一约束的附加属性。

1 <many-to-one name="Employee" class="Employee" column="Employee_Id" unique="true" />

下面的图应该可以帮助你关联这个映射如何关联域模型和数据库表:

 

 

6.5.多对多关联

在数据库中,多对多关联就是通过中间表连接起来的两个one-to-many关联的结构。我们Domain工程里的员工-社团示例,可以创建为如下表结构:

 

如您所见,我们有一个名为Employee_Community的中间表。这里有两个one-to-many的关联,一个是从Employee到Employee_Community 另一个是从Community 到 Employee_Community 。我们想要的最终效果是,从Employee到Community ,可能得到多个Community ,反之亦然。这样就得到了many-to-many关系。

 
下面的单元测试验证了我们在将员工映射到社团关联时想要的行为:
 1 [Test]
 2 public void MapsCommunities()
 3 {
 4     object id = 0;
 5     using (var transaction = session.BeginTransaction())
 6     {
 7         id = session.Save(new Employee
 8         {
 9             EmployeeNumber = "123456789",
10 
11             Communities = new HashSet<Community>
12             {
13                 new Community
14                 {
15                     Name = "Community 1"
16                 },
17                 new Community
18                 {
19                     Name = "Community 2"
20                 }
21             }
22         });
23         transaction.Commit();
24     }
25     session.Clear();
26     using (var transaction = session.BeginTransaction())
27     {
28         var employee = session.Get<Employee>(id);
29         Assert.That(employee.Communities.Count, Is.EqualTo(2));
30         Assert.That(employee.Communities.First().Members.First().
31         EmployeeNumber, Is.EqualTo("123456789"));
32         transaction.Commit();
33     }
34 }

在这里,我们存储了一个Employee对象和两个Community 对象。然后检索保存的Employee实例,并验证其上有两个Community实例。我们还验证community在其成员集合中有雇员实例。可能还有更多的东西我们可以在这里验证,但为了测试关联,我认为这已经足够了。

 

这个关联的映射需要同时添加到EmployeeCommunity 。这是因为关联是双向的。下面是添加到employee.hbm.xml的映射:

 

1 <set name="Communities" table="Employee_Community" cascade="alldelete-orphan">
2     <key>
3         <column name="Employee_Id" />
4     </key>
5     
6     <many-to-many class="Community">
7         <column name="Community_Id" />
8     </many-to-many>
9 </set>

接下来是添加到community.hbm.xml的映射的另一部分:

1 <set name="Members" table="Employee_Community" cascade="all-deleteorphan" inverse="true">
2     <key>
3         <column name="Community_Id"/>
4     </key>
5     
6     <many-to-many class="Employee">
7         <column name="Employee_Id" />
8     </many-to-many>
9 </set>

前面的映射与我们前面看到的集合映射非常相似。这并不奇怪,因为我们实际上是在处理两个集合映射。让我们注意一下这个映射与集合映射之间的关键区别。

  • 第一个区别是set节点上的附加table 属性。这个属性告诉NH,中间表(作为两个实体表的连接表)的名字是什么,如Employee_Community 。
  • 第二个区别是set节点内的 many-to-many 节点,和在集合映射中,我们有one-to-many。many-to-many节点的两个必需信息是class 和 column属性 class告诉关联另一端的类的名字。在employee映射中这是Community,在community中这是Employee。column用于声明外键列的名称(中间表里)对于employee,是列Community_Id。对于community,是Employee_Id列

 

接下来的图片可能会有所帮助。下面的第一张图片显示了Employee 到 Community 的关联映射:

 

 

 接下来,第二张图片显示了Community 到 Employee  的关联映射:

 

 

 与集合映射类似,这里使用了set作为示例,您可以使用map、bag、list、array等。

小贴士:

我们在Community端向映射中添加了inverse属性。此属性掌管谁该拥有关联。我打算按下不表,在第5章详细讨论这个问题。

至此我讲完了本章的关联映射。我们在这里学到的东西应该能打下良好的知识基础。关联是一个广阔而重要的领域。我们将在整本书中不断回顾关联映射,并在这个过程中学习更多关于它们的知识。现在让我们转到下一个话题。

 

posted on 2020-07-11 17:52  困兽斗  阅读(125)  评论(0)    收藏  举报

导航