持久性无知和 DDD 现实

Posted

技术标签:

【中文标题】持久性无知和 DDD 现实【英文标题】:Persistence ignorance and DDD reality 【发布时间】:2013-08-23 10:08:13 【问题描述】:

我正在尝试不费吹灰之力地实现完全有效的持久性无知。不过我有很多问题:

最简单的选择

这真的很简单——可以像在 SOA 中一样使用 Spring Data 注释对实体进行注释(但让它们真正执行逻辑)?除了必须在实体中使用持久性注释之外还有什么后果,这并不真正遵循 PI 原则?我的意思是 Spring Data 真的是这样 - 它提供了很好的存储库,可以完成 DDD 中的存储库应该做的事情。那么问题在于实体本身......

更难的选择

为了使实体不知道它所操作的数据来自哪里,很自然地通过构造函数将该数据作为接口注入。另一个优点是我们总是可以执行延迟加载——例如,我们在 Neo4j 图形数据库中默认拥有。缺点是聚合(由实体组成)将完全了解所有数据,即使它们不使用它们 - 由于数据完全暴露,可能会导致调试困难(DAO 将像聚合一样是分层的)。这也将迫使我们为存储库使用一些适配器,因为它们不再存储真实的实体......而且任何翻译都很丑陋......另一件事是我们无法在没有这样的 DAO 的情况下实例化实体 - 尽管可能存在-域中的内存实现......再次,更多层。有人说注入 DAO 也会破坏 PI。

最难的选择

实体可以包裹在一个决定数据应该来自哪里的延迟加载器上。它可以是内存中的,也可以是数据库中的,它可以处理任何需要事务的操作等等。虽然是复杂的层,但在某种程度上可能是通用的......?阅读一下here

您知道其他解决方案吗?或者,也许我在提到的那些中遗漏了一些东西。请分享您的想法!

【问题讨论】:

【参考方案1】:

就我个人而言,在尝试使用与 ORM 相同的实体时,几乎不可能实现干净的域模型。

我的解决方案是按照我认为合适的方式对我的域实体进行建模,并确保任何 ORM 实体都不会泄漏到存储库之外。这意味着我的存储库接受并返回域实体。

这意味着您失去了“大部分 ORM 优点”并最终“使用您的 ORM 进行简单的 CRUD 操作”。

这两种权衡对我来说都很好,我宁愿有一个我可以使用的干净的域模型,而不是一个被我的数据库或 ORM 中的人工制品污染的域模型。它还将我“与我的 ORM 搏斗”所花费的时间减少到零。

附带说明,我发现文档数据库更适合 DDD。

【讨论】:

像 Neo4j 这样的图形数据库怎么样? 听起来不错,我以前从未见过 Neo4j,但它看起来与大多数文档数据库的概念相同(您的普通文档数据库会将对象图扁平化为 JSON)。 图形数据库真的很强大。也许整个 Persistence Ignorance 概念需要从这个角度重新考虑...... 如何处理需要从持久存储中读取的简单实体?为什么不在只读取数据的情况下公开 DAO? 如果我使用 EF,我通常通过服务调用存储库上的 GetThing() 方法。如果我使用的是文档数据库,那么我可以从我喜欢的任何层进行查询,因为我的数据库包含序列化的聚合根,所以根本不需要 DAO 或存储库。【参考方案2】:

作为正确领域建模的副作用,我(几乎)免费实现了持久性无知。

特别是:

如果您正确定义每个上下文的边界,您将获得不需要延迟加载的小实体(这实际上成为 DDD 项目中的反模式/代码味道) 如果您不能简单地将 SQL 用于存储库,请将一组 DTO 映射到您的 db 架构,然后将它们用于工厂以初始化实体类。

在 DDD 项目中,持久性无知与领域模型本身有关,与存储库、工厂和其他应用程序代码无关。事实上,您将来不太可能更改 ORM 和/或 DB。

唯一(但非常强)对领域模型的持续无知背后的理性是关注点分离:在领域模型中,您应该only表达业务不变量!持久性是一个基础设施问题!

例如,如果没有持久性无知(和延迟加载),域模型应该处理来自数据库的可能异常,它的复杂性会增加,并且业务规则会被隐藏在技术细节之下。

【讨论】:

能否请您编写适用于您所说内容的示例域对象、工厂和存储库?如果没有看到(伪)代码,我很难理解这个概念。您的意思是域对象是具有正常字段集的简单类吗?那么如何从它们中浇水/提取数据呢?有人建议的休眠映射? 从技术上讲,域对象应该是简单的POJO/POCO。否则你最终会混合不同的关注点。持久性应该是存储库观察到的events 的效果。 因此,正如我所见,一旦从数据库中检索到对象,就无需再执行诸如 repository.save(domainObject) 之类的操作,因为它应该在其某些数据被修改时自行发布事件存储库将立即处理哪个? 没错。我已经在一些复杂的现实世界 DDD 项目中测试了这种方法,并且效果很好。但是,我们有定制的基础设施来降低成本。 你的意思是像this这样的吗?【参考方案3】:

除非撞到石墙,否则我会选择最简单的方法。采用pi原理也有this等陷阱。

有时一些妥协是可以接受的。

public class Order 
    private String status;//my orm does not support enum

    public Status status() 
        return Status.of(this.status);
    

    public is(Status status) 
        return status() == status;//use status() instead of getStatus() in domain model
    

【讨论】:

【参考方案4】:

一旦您将在域模型中提供持久性映射:

您的代码取决于框架。如果您决定更改此框架,您希望更改持久层和模型层源代码 - 更多工作、更多更改、更多代码合并等。 您的域模型 jar 文件依赖于 spring/nhibernate jar 等。 您的类越来越大,业务代码和持久性相关代码如何增长

我不得不承认我不理解 更难和最难的选项

我们为域实体使用了分离的接口和实现。使用 Hibernate 和存储库提供分离的映射文件。

实体是使用工厂(或以后的存储库)创建的,标识符是在持久层中生成的,实体在持久化之前不需要它。

延迟加载由List 的特殊实现提供一次:

    实体的映射包含它 从持久层获取实体/聚合

唯一的问题与事务有关,因为当您在事务范围之外使用延迟加载的集合时,它会失败。

【讨论】:

好的,所以你的意思是实体应该是完全没有外部数据概念的普通类(它们应该只有普通字段),我应该为每个实体是吗? Hibernate 应该处理映射,对吗?能否给我一个可靠的链接? 是的,我喜欢这种方法 :-) 您有时需要调整域模型实体,例如因为类的休眠要求(例如私有无参数构造函数)。我不喜欢外部数据的方法,因为恕我直言,您的意思是提供一些 DTO,对吗?这可能是创建而不是 GC 实体的性能问题。看docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/…它从映射类型的定义开始 我相信映射也有一些性能影响:)。无论如何,我喜欢拥有正常的、未注释的域类的概念。我指的是界面后面的 DAO(所以它是实体)。还有一件事——我应该用实体、聚合和值对象的术语来命名我的域对象还是在数据库中做? 在使用 DDD 方法时,我更喜欢使用存储库模式,请参阅***.com/questions/8550124/…。我不在类名中使用这些术语。你为什么要这样做?通常实体具有标识符,但值对象由属性/字段定义 - 您可以一眼就看到它。通常,作为域对象层次结构的根,您几乎没有聚合,因此这是您团队中的“众所周知的信息”。如果你想用这些术语来标记你的类,你应该使用 cmets。 明白:)。因此,在我的域项目中,我应该包括:具有所有域逻辑的所有域对象(其中包含简单的数据,就像没有数据库一样)、抽象存储库(接口,允许我做一些事情,比如添加一个域对象并通过某些东西检索它与它相关)和一个工厂......好吧 - 它应该像接口一样是抽象的,还是我应该实现它......?那么测试呢?

以上是关于持久性无知和 DDD 现实的主要内容,如果未能解决你的问题,请参考以下文章

以下代码是不是违反持久性无知规则

DDD - 持久性模型和领域模型

究竟啥是“执着无知”?

没有getter的DDD对象持久性

DDD 域实体与持久性实体

DDD with L2S or NHibernate... 关于业务对象的持久化数据