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

7.映射继承

您的领域模型中经常会有父类、子类。在我们的领域模型中,我们所有具体福利继承自Benefit父类。下面是一个精简版的类图,来自我们的领域模型,表示这种继承关系:

 

 

可以像映射任何其他类一样将这些类映射到数据库表。但在某些情况下,你会希望NHibernate在开箱即用的情况下为你做更多的事情。其中一种情况涉及多态关联。

 

多态关联是这样一种关联:其中一端被声明为基类类型,但在运行时它持有派生类型之一的实例。例如,在我们的领域模型中,从Employee到Benefit类的关联就是多态关联的一个例子:

1 public class Employee : EntityBase
2 {
3     public virtual ICollection<Benefit> Benefits { get; set; } //基类集合
4 }

在这里,尽管Employee类上声明的集合属性Benefits是基类类型,但我们知道,在运行时,该集合中Benefit类的特定实例可以是任何派生类型。如果在代码中构建此集合,则可以对其进行编码,以便将正确类型的实例添加到集合中。但是NHibernate呢?NHibernate能在加载数据库时添加正确类型的实例吗?

幸运的是,这个问题的答案是肯定的。如果我们使用NH提供的一种继承映射策略来映射继承家族,NHibernate能够处理多态关联。

 

小贴士:

可以使用其中一种继承映射策略映射任何继承类,但可能不需要这样做。例如,我们所有的域类都继承自类EntityBase,但不需要映射它们。这样做的主要原因是这些类从来没有在多态关联上下文中使用过,因此没有必要让NHibernate知道这些子类是从其他父类继承来的。另外,我们引入这种继承是因为我们注意到Id属性在所有类上都是重复的。实际上,从EntityBase继承的所有域类并不代表真正意义上的继承家族。(主要是为了提升Id属性到父类,减少重复代码)

 

为了让NHibernate处理多态关联,继承家族必须使用NHibernate继承映射策略中的一种。NHibernate支持以下三种基本策略:

  • Table per class hierarchy 父类和所有子类共用一表(靠字段区分)
  • Table per subclass 父类及所有子类各有一表(父类子类依靠主键关联)
  • Table per concrete class 父类及所有子类各有一表(父类子类无关联)

除了前面的三个,NHibernate还支持最后一个“Table per concrete class”的变体。这称为隐式多态。我们不会在本章中讨论这个问题,我们鼓励读者自己去探索这个问题。如果你真的需要的话,NHibernate也允许你混合使用这些策略。我们将很快进入前面每个策略的细节,并查看如何使用这些策略映射我们的受益类。在此之前,让我们看一下将用于验证映射的单元测试。

 

7.1.单元测试验证继承映射

为了测试映射是否正确,我们应该能够保存一个从Benefit类继承的类的实例,并能够成功地检索它。代码如下:

 1      [Test]
 2      public void MapsSkillsEnhancementAllowance()
 3         {
 4             object id = 0;
 5             using (var transaction = session.BeginTransaction())
 6             {
 7                 id = session.Save(new SkillsEnhancementAllowance
 8                 {
 9                     Name = "Skill Enhacement Allowance",
10                     Description = "Allowance for employees so that their skill enhancement trainings are paid for",
11                     Entitlement = 1000,
12                     RemainingEntitlement = 250
13                 });
14                 transaction.Commit();
15             }
16             session.Clear();
17 
18             using (var transaction = session.BeginTransaction())
19             {
20                 var benefit = session.Get<Benefit>(id);
21                 var skillsEnhancementAllowance = benefit as SkillsEnhancementAllowance;
22                 Assert.That(skillsEnhancementAllowance, Is.Not.Null);
23                 if (skillsEnhancementAllowance != null)
24                 {
25                     Assert.That(skillsEnhancementAllowance.Name, Is.EqualTo("Skill Enhacement Allowance"));
26                     Assert.That(skillsEnhancementAllowance.Description, Is.EqualTo("Allowance for employees so that their skill enhancement trainings are paid for"));
27                     Assert.That(skillsEnhancementAllowance.Entitlement, Is.EqualTo(1000));
28                     Assert.That(skillsEnhancementAllowance.RemainingEntitlement, Is.EqualTo(250));
29                 }
30                 transaction.Commit();
31             }
32         }

如您所见,我们正在存储SkillsEhancementAllowance类的实例。然后我们告诉NHibernate用我们保存SkillsEnahcementAllowance类的实例时返回的id来检索一个Benefit类的实例。在这个检索到的Benefit类实例上,我们验证了三件事:

  • 检索到的实例实际上是具体子类:SkillsEnhancementAllowance类的类型
  • 父类 Benefit 类的所有属性都被正确地检索
  • 子类 SkillsEnhancementAllowance类的所有属性都被正确检索

这里要注意的有趣的事情是,我们告诉NHibernate加载Benefit类,这是一个基类,但是NHibernate聪明地加载了正确的派生类实例。这是NHibernate提供的多态行为。实际上我有三个这样的测试,每个从Benefit类派生的类都有一个。但是为了简洁起见,我在这里只介绍了一个。其他的与此非常相似,您可以在本书的源代码中查看它们。

让我们通过编写一些继承映射来通过这些测试。

 

 

 

7.2.父类和所有子类共用一表(靠字段区分)

当我第一次使用NHibernate时,我很难记住每个策略的含义。造成混淆的原因之一是我把“类层次”这个词与错误的东西联系在一起。我不会详细说明我所做的假设,因为这与本文的讨论无关,但我想告诉您,单词“类层次结构”指的是从一个最终基类继承的类家族。比如我们例子里的:Benefit, LeaveSkillsEnhancementAllowance, SeasonTicketLoan 这些类在一起,构成了继承家族。

如果您的继承层次有很多阶,则层次结构将覆盖所有继承级别上的所有类。现在您可能已经猜到“每个类层次表”提供了什么。该策略将层次结构中的所有类映射到数据库中的一个表中。下面是我们如何使用这种策略映射收益类层次结构:

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Domain" namespace="Domain">
 3     <class name="Benefit">
 4         <id name="Id" generator="hilo" />
 5         <discriminator column="BenefitType" type="String" />
 6         <property name="Name" />
 7         <property name="Description" />
 8         <many-to-one name="Employee" class="Employee" />
 9         <subclass name="SkillsEnhancementAllowance" discriminatorvalue="SEA">
10             <property name="RemainingEntitlement" />
11             <property name="Entitlement" />
12         </subclass>
13         <subclass name="SeasonTicketLoan" discriminator-value="STL">
14             <property name="Amount" />
15             <property name="MonthlyInstalment" />
16             <property name="StartDate" />
17             <property name="EndDate" />
18         </subclass>
19         <subclass name="Leave" discriminator-value="LVE">
20             <property name="Type" />
21             <property name="AvailableEntitlement" />
22             <property name="RemainingEntitlement" />
23         </subclass>
24     </class>
25 </hibernate-mapping>

虽然我在一个XML文件中单个class节点下声明了所有映射,但是您可以将每个子类映射放到它自己的XML文件中。NHibernate会使用自己的逻辑来编织来自多个文件的映射。这适用于所有继承映射策略。

这种映射将使我们能够将所有Benefit类中的数据存储到如下所示的表中:

 

如您所见,该表有对应所有四个映射类的所有属性的列。让我们仔细看看这些映射。

基类的映射与其他类一样正常。甚至派生类的属性映射也很常见。这里有两件新事情:

元素discriminator 用于在数据库表中声明将用作discriminator 的列。区别旗标,顾名思义,区别表的不同行。换句话说,列的值告诉这一行属于层次结构中的哪个类。当从表中检索行时,NHibernate使用这个列来生成正确的类。在discriminator元素上,我们声明了两个实际上是可选的属性。属性column 用于声明discriminator 列的名称。属性type声明discriminator列的类型。如果没有声明,NHibernate会假设这些属性的默认值,分别是class和string

 

元素subclass有两个用途。第一,它充当根元素,派生类的属性按通常方式映射在根元素之下。其次,它允许您指定派生类的名称,以及discriminator列中用于标识属于该类的数据库记录的值。

以下是关于这个策略需要注意的一些重要事情:

  • 此策略映射到未规范化的数据库表。
  • 需要一个称为discriminator 的附加列。这个列不存储任何业务关键信息,仅仅是NHibernate使用它来决定从表中获取一条记录时应该实例化哪个类。
  • 不可能将派生类的属性标记为not-null。这是因为当属于一个类的记录插入到表中时,映射到其他类的列必须保持为空。

7.3.父类及所有子类各有一表(父类子类依靠主键关联)

“每个类层次表”中的大多数问题都是通过“每个子类表”策略解决的。在这种策略中,每个类都被映射到它自己的表中,但有一个警告,我们将在后面介绍。让我们首先看看这个策略的映射,如下所示:

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Domain" namespace="Domain">
 3     <class name="Benefit">
 4         <id name="Id" generator="hilo" />
 5         <property name="Name" />
 6         <property name="Description" />
 7         <many-to-one name="Employee" class="Employee"/>
 8         <joined-subclass name="SkillsEnhancementAllowance">
 9             <key column="Id" />
10             <property name="RemainingEntitlement" />
11             <property name="Entitlement" />
12         </joined-subclass>
13         <joined-subclass name="SeasonTicketLoan">
14             <key column="Id"/>
15             <property name="Amount" />
16             <property name="MonthlyInstalment" />
17             <property name="StartDate" />
18             <property name="EndDate" />
19         </joined-subclass>
20         <joined-subclass name="Leave">
21             <key column="Id"/>
22             <property name="Type" />
23             <property name="AvailableEntitlement" />
24             <property name="RemainingEntitlement" />
25         </joined-subclass>
26     </class>
27 </hibernate-mapping>

如您所见,这里没有鉴别器列。NHibernate能够区分属于不同派生类的数据库记录,而不需要借助于鉴别器列。NHibernate在正确的表上使用连接来获取完整的数据库记录。这是映射被称为连接子类的另一个原因。这个映射将映射到下面的表模式:

 

 这里,层次结构中的每个类都有一个表。这里需要注意的一点是,每个表都有一个Id列,它也是各自表上的主键。这个主键在基类的表和派生类的表之间共享。这一策略需要注意的要点是:

 

  • 因为每个类都映射到它自己的表,所以可以在派生类上创建列not-null,这与“每个类层次都有表”不同。
  • NH在从这些表中检索记录时使用连接。这有一点性能开销

 

7.4.父类及所有子类各有一表(父类子类无关联)

您可以简单地将所有四个Benefit类映射到它们自己的表中,而不需要任何继承映射语义。但是这种方法的另一面是您将不能使用多态关联。如果您仍然希望将每个类映射到它自己的表,并且能够使用多态关联,那么“每个具体类对应表”是您要找的策略。Benefit 类的继承家族映射如下:

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
 3 assembly="Domain" namespace="Domain">
 4     <class name="Benefit">
 5         <id name="Id" generator="hilo" />
 6         <property name="Name" />
 7         <property name="Description" />
 8         <many-to-one name="Employee" class="Employee" />
 9         <union-subclass name="SkillsEnhancementAllowance">
10             <property name="RemainingEntitlement" />
11             <property name="Entitlement" />
12         </union-subclass>
13         <union-subclass name="SeasonTicketLoan">
14             <property name="Amount" />
15             <property name="MonthlyInstalment" />
16             <property name="StartDate" />
17             <property name="EndDate" />
18         </union-subclass>
19         <union-subclass name="Leave">
20             <property name="Type" />
21             <property name="AvailableEntitlement" />
22             <property name="RemainingEntitlement" />
23         </union-subclass>
24     </class>
25 </hibernate-mapping>

正如元素union-subclass 所建议的,该映射策略在所有四个表上运行SQL union,以便检索完整的数据库记录。此策略映射的数据库表如下图所示。你可以在下图中看到,NHibernate已经自动将基类中的属性重新映射到对应于派生类的表中:

 

每个类都映射到一个独立的表。表之间没有连接。基类的属性在派生类的每个表上重新映射。所以这些表不是完全标准化的形式。unions 的使用可能预示着性能的下降。这种策略的一个严重问题是多态关联不能可靠地工作。它们可能在某些情况下有效,但并不总是如此。此策略的另一个限制是不能使用identity 作为标识生成策略

这是因为每个表都有自己的identifer列。如果我们使用标识,那么我们会得到跨表的重复标识值。如果你尝试使用一个标识值来加载Benefit 实例,NHibernate会感到困惑,因为会有多个记录匹配给定的标识值。从另一个角度来看,这是一件好事,因为无论如何,identifer都不是推荐的标识生成策略。

 

小贴士:

Benefit表中有映射到Benefit实体的列。这些相同的列也出现在派生实体的表中。这是多余的。我们可以通过使用可应用于类元素的名为abstract的属性来停止使用收益表。值为true意味着从NHibernate的角度来看,类可以被视为抽象。NHibernate然后生成所有SQL,如果没有数据库中的收益表。

 

注意,"table per concrete class"  策略可以使用隐式多态性实现。这与我们在本节开始时讨论的方法类似,在开始时我们说过,每个派生类都可以相互独立地映射到数据库表。在这种方法中,您不能使用多态关联,因此只有在无法使用任何其他策略时,我才会使用此策略。

 

7.5.正确选择映射策略

应该使用哪种继承策略?这不是一个容易回答的问题。不止一个因素在起作用,你应该在做决定之前逐个考虑这些因素。

 

如果您正在使用遗留数据库,并且您不能更改现有的数据库模式,那么您必须选择允许您将域模型映射到模式的策略。在这里你没有很多选择,但坚持一个可以完成工作。有时,由于您无法控制的原因,您甚至可能需要混合使用不同的策略以使映射工作。NHibernate的美妙之处在于,它可以让你混合使用不同的策略,而不用担心NHibernate如何从表中存储和检索数据。

 

如果你正在做一个green field 项目中,可以自由定义自己的数据库结构,那么你可以看看其他因素如偏好规范化和非规范化数据库模式必须执行哪些操作,预期的性能,要求表列设置为NOT NULL,等等。

 

 

8.映射组件

 

 

 

9.代码映射

 

 

9.1.标识

 

 

9.2.属性

 

 

9.3.关联

 

 

 

9.3.1 一对多

 

 

9.3.2 多对一

 

 

9.3.3 一对一

 

 

9.3.4 多对多

 

 

 

9.4. 组件

 

 

9.5. 继承

 

 

9.5.1. 父类和所有子类共用一表(靠字段区分)

 

 

9.5.2. 父类及所有子类各有一表(父类子类依靠主键关联)

 

 

9.5.3. 父类及所有子类各有一表(父类子类无关联)

 

 

 

9.6. 完整的代码映射

 

 

 

10.流畅映射 Fluent mapping

 

 

10.1.继承映射

 

 

10.1.1. 父类和所有子类共用一表(靠字段区分)

 

 

 

10.1.2. 父类及所有子类各有一表(父类子类依靠主键关联)

 

 

 

 

10.1.3. 父类及所有子类各有一表(父类子类无关联)

 

 

 

10.2. 组件映射

 

 

 

 

11.正确选择何种映射

 

 

 

 

12.本章总结

 

 

 

 

 

阿松大

posted on 2020-07-16 13:49  困兽斗  阅读(114)  评论(0)    收藏  举报

导航