值对象的层次结构

栏目: ASP.NET · 发布时间: 5年前

内容简介:有两个类:Person  和Document,具有以下业务规则:此领域模型中有两种类型的文档:IdentityCard和Passport。以下是领域模型在图表上的外观:

有两个类:Person  和Document,具有以下业务规则:

  • 一个Person  可以拥有零个或一个Document。
  • Document只能分配给一个Person  。
  • 没有Person就没有Document。

此领域模型中有两种类型的文档:IdentityCard和Passport。以下是领域模型在图表上的外观:

值对象的层次结构 就底层数据库而言,第一个想法是:大多数人(包括我)都会为Person引入一个表,为Document引入另一个表。ER模型表如下:

值对象的层次结构

这里是代码中的领域模型(公共setter是为了简洁):

<b>public</b> <b>class</b> Person : Entity
{
    <b>public</b> virtual string Name { get; set; }
    <b>public</b> virtual Document Document { get; set; }
}

<b>public</b> <b>class</b> Document : Entity
{
}

<b>public</b> <b>class</b> IdentityCard : Document
{
    <b>public</b> virtual DateTime IssueDate { get; set; }
}

<b>public</b> <b>class</b> Passport : Document
{
    <b>public</b> virtual string SerialNumber { get; set; }
}

virtual 关键字是使用了NHibernate.

该解决方案看起来不错,但此实现的问题是,如果您尝试为一个人分配一个新文档,旧文档仍将保留在数据库中,它将不会自动删除。所以,像这样:

<b>var</b> person = <b>new</b> Person { Name = <font>"Name"</font><font> };
person.Document = <b>new</b> IdentityCard(DateTime.Now);
Flush(); </font><font><i>// both the person and the document are created</i></font><font>

person.Document = <b>new</b> Passport(</font><font>"Serial Number"</font><font>);
Flush(); </font><font><i>// the new document is assigned but the old one remains in the database</i></font><font>
</font>

导致在数据库中有两个Document。其中只有一个会被一个人引用,另一个会成为孤儿。您必须使用存储库或直接调用NHibernate会话手动删除第二个Document。

NHibernate级联选项

如何解决这个问题?

NHibernate具有丰富的级联功能。您可以使用它们来通知NHibernate在创建,更新或删除父实体时如何处理子实体。以下是最受欢迎的级联选项:

  • None 什么也不做.
  • Save-update 在创建或更新父实体时创建或更新子实体。
  • All — 类似save-update 加deletion. 意味着如果删除父实体,则会删除子实体。
  • All-delete-orphans 全删除孤儿 — t与所有  孤立实体的删除相同

All-delete-orphans 对Childern实体集合有用,Person类如果有Document列表这个子集合,所有的Document只会被与Person本身被删除; 使用all-delete-orphans,你也可以从Person的Document集合中删除一个document ,达到数据表里面的document被删除,无需删除Person!

All-delete-orphans 这个选项看起来像是我们问题的完美匹配,但这就是问题所在。它只适用于一对多关系。如果Person  有一个文档集合,那就是它 - 只需修改映射文件并完成它。但在我们的案例中,一个Person只能拥有一个Document,这是一对多(或一对一)的关系,而不是一对多关系。

值对象的层次结构

那么该怎么办?

在继续解决之前,让我们再看一下领域模型:

什么是Document,实体或价值对象?

实体与价值对象不同的主要判断依据是:是否有必要跟踪它。(banq注:如果需要跟踪,就需要唯一标识,没有唯一标识就无法跟踪):

如果您需要知道领域模型中特定对象发生了什么,并且即使在修改它之后也将该对象与其他对象区分开来,那么这就是一个实体。如果您可以将另一个替换为另一个,那么这是一个值对象。

在我们的领域模型中,Document只能分配给单个Person。此外,Document本身不能单独存在!这是一个强有力的指标,表明Document  是一个值对象。当您替换旧Document时,不必关心旧Document会发生什么。此外,如果需要删除旧的Document,以免它遗弃在数据库。这就是值对象的属性。

顺便说一下,Document是值对象,这是一个好的案例,因为它表明概念本身不能说明它是一个实体或一个值对象,而是取决于特定应用/有界背景(限定上下文/有界上下文)的要求。在另一个有限的上下文中,Document  很可能是一个实体。

在数据库中存储值对象的最佳方法是将它们包含在父表中。这就是我们在这里需要做的事情。

将Document的数据与Person合并 是一种自然的解决方案,它允许您使Document  遵循值对象语义。就像您可以用 您想要的任何其他名称替换Person的名称,并且不必担心从数据库中删除旧名称,您也可以轻松地用新的名称替换Person的文档,忘记现有实施的所有令人头疼的问题。

请记住:因为单独的表用来实现实体,如果Document  是一个实体,就需要单独表来实现(banq注:实体可用来修改,因为可以通过唯一标识ID查询到定位到这个实体,这种根据ID查询定位本身就是一种跟踪,所以,实体一般可以修改,而值对象只需要插入和删除两个动作,只有实体才需要CRUD四个动作,如果你希望将值对象放在单独表,那么就要给予其唯一标识ID,那么这个表的操作只有插入和删除两个动作,这也是一种变通方式)。

好吧,所以我们需要将所有数据从Document表移动到Person中。这就是它的样子:

值对象的层次结构

但是有一个问题:文档的层次结构。如果不再有单独的文档表,那么如何将该层次结构映射到数据库?内置的ORM继承功能仅适用于实体。更具体地说,此功能需要单独的表或几个单独的表。

我们在这里需要一个自定义继承实现 - 一个包含文档层次结构的包装类。我们将使用此包装容器代替Person中的文档,如下所示:

<b>public</b> <b>class</b> DocumentContainer
{
    <b>private</b> DocumentType _type;
    <b>private</b> DateTime? _issueDate;
    <b>private</b> string _serialNumber;

    <b>public</b> Document Document
    {
        get
        {
            <b>switch</b> (_type)
            {
                <b>case</b> DocumentType.IdentityCard:
                    <b>return</b> <b>new</b> IdentityCard(_issueDate.Value);

                <b>case</b> DocumentType.Passport:
                    <b>return</b> <b>new</b> Passport(_serialNumber);

                <b>default</b>:
                    <b>throw</b> <b>new</b> ArgumentOutOfRangeException();
            }
        }
        set
        {
            <b>switch</b> (value)
            {
                <b>case</b> IdentityCard identityCard:
                    _issueDate = identityCard.IssueDate;
                    _serialNumber = <b>null</b>;
                    _type = DocumentType.IdentityCard;
                    <b>break</b>;

                <b>case</b> Passport passport:
                    _issueDate = <b>null</b>;
                    _serialNumber = passport.SerialNumber;
                    _type = DocumentType.Passport;
                    <b>break</b>;
            }
        }
    }
}

<b>public</b> <b>class</b> Person : Entity
{
    <b>public</b> virtual string Name { get; set; }

    <b>private</b> readonly DocumentContainer _document;
    <b>public</b> virtual Document Document
    {
        get => _document.Document;
        set => _document.Document = value;
    }
}

所以基本上我在这里做的是我重新实现了开箱即用的ORM中可用的每表类层次映射。包装容器包含层次结构中所有类的所有字段,并根据值对象的类型更新或取消它们。

这种手动映射看起来很笨拙但很好的部分是它隐藏在Person类的客户端之外。Person 的表达式好且干净的,Document 实例是一个正确的值对象 - 没有身份标识,而且其生命周期完全由父实体控制。

完整代码: https://gist.github.com/vkhorikov/61f873671630db5a4e0234f9912c660e

这是我在很多复杂场景中遇到的重现主题。如果您无法弄清楚如何使用普通ORM映射某些内容,请创建一个包装器并映射该包装器。只需确保不要使用类的公共API公开它。

总结

  • NHibernate不支持 多对一或一对一关系中的all-delete-orphans级联功能。只有一对多关系才有资格。
  • 在数据库中存储值对象的最佳方法是将它们嵌入到父实体的表中,即使它不仅仅是单个值对象而是它们的层次结构。
  • 要实现值对象的层次结构,请使用层次结构中所有值对象的所有数据字段创建包装类。将该类映射为常规值对象。
  • 不要将包装器暴露给类的客户端。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员面试宝典

程序员面试宝典

欧立奇、刘洋、段韬 / 电子工业出版社 / 2006-7 / 39.00元

本书取材于各大IT公司历年面试真题(包括笔试题、口试题、电话面试、英语面试,以及逻辑测试和智商测试)。通过精确详细的分类,把在应聘程序员(含网络、测试等)过程中所遇见的常见考点分为21章。不仅对传统的C系语言考点做了详尽的解说,包括面向对象问题、sizeof问题、const问题、数据结构问题等。还根据外企出题最新特点,针对设计模式问题、C#问题、网络问题、数据库问题、NET问题等,做了深入的说明。......一起来看看 《程序员面试宝典》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具