内容简介:有两个类: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级联功能。只有一对多关系才有资格。
- 在数据库中存储值对象的最佳方法是将它们嵌入到父实体的表中,即使它不仅仅是单个值对象而是它们的层次结构。
- 要实现值对象的层次结构,请使用层次结构中所有值对象的所有数据字段创建包装类。将该类映射为常规值对象。
- 不要将包装器暴露给类的客户端。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。