Dapper 的 DDD 原则和存储库

Posted

技术标签:

【中文标题】Dapper 的 DDD 原则和存储库【英文标题】:DDD principles and repositories with Dapper 【发布时间】:2019-06-26 18:38:11 【问题描述】:

我正在建立一个应用洋葱架构和 DDD 模式的解决方案。

DDD 原则之一鼓励域实体仅具有私有设置器和私有默认构造函数,以确保您无法在无效状态下创建域实体。

存储库包含域实体上的数据操作,这些操作从/映射到数据库。我一直在尝试以下两种方法:

    以纯粹方式的域实体:没有默认构造函数,没有公共设置器;验证在构造函数中完成;这确保您不能在无效状态下创建域实体。副作用是在读操作中更难在存储库中将它们非实体化;因为您需要反射才能创建实例和映射属性;以及在需要映射到实际域实体的 Dapper 请求中使用动态。如果我不使用动态将其直接映射到域实体,Dapper 会抛出没有公共构造函数的异常

    以非纯粹方式的域实体:您允许默认构造函数,并且所有 setter 都是公共的;因此您可以创建在给定时间点无效的实体。在这种情况下,您需要手动调用 Validate() 方法,以确保它们在继续之前有效。这使得存储库中的去材料化变得更加容易,因为您不需要反射或动态来将数据库映射到模型。

这两种方法都有效,但是,使用选项 2,存储库变得更加简单,因为它们包含的自定义映射代码要少得多,而且没有反射显然也会提高性能。当然,DDD 并不是以纯粹的方式应用的。

在决定我将在我的项目中使用什么之前,有一个问题:是否有任何其他 micro-ORM 框架可以处理私有构造函数和设置器,以便将数据库映射到这些类型在没有额外的自定义映射逻辑的情况下支持“纯”域实体? (没有 EF 也没有 NHibernate,我想要一些轻量级的东西)。

或其他技术解决方案来保持“纯”模型实体方法与简单的存储库映射相结合?

编辑:我实施的解决方案如下。

首先,域实体中的构造函数和设置器都是“内部”的,这意味着它们不能由域模型的消费者设置。但是,我使用“InternalsVisibleTo”来允许数据访问层直接访问它们,因此这意味着使用 Dapper 从数据库中去实体化非常容易(不需要中间模型)。从应用层来说,我只能用领域方法来改变领域实体,不能直接改变属性。

其次,为了从我的应用层构建新的域实体,我添加了流畅的构建器来帮助构建域实体,所以我现在可以像这样构建它们:

 User user = new UserBuilder()
        .WithSubjectId("045454857451245")
        .WithDisplayName("Bobby Vinton")
        .WithLinkedAccount("Facebook", la => la.WithProviderSubjectId("1548787788877").WithEmailAddress("bobby1@gmail.com"))
        .WithLinkedAccount("Microsoft", la => la.WithProviderSubjectId("54546545646").WithEmailAddress("bobby2@gmail.com"))

当构建器“构建”实体时,验证也会完成,因此您永远不能创建处于无效状态的实体。

【问题讨论】:

你会选择什么,为什么?还有其他的吗...? Are not valid questions on *** 【参考方案1】:

DDD 原则之一鼓励域实体仅具有私有设置器和私有默认构造函数,以确保您无法在无效状态下创建域实体。

这不太对。是的,富域模型通常不公开 setter,但那是因为它们不需要 setter。你告诉模型在更高的抽象层次上做什么,并让它决定如何修改它自己的数据结构。

同样,经常在某些情况下暴露默认构造函数是有意义的:如果您将聚合视为有限状态机,那么默认构造函数是一种在它的“开始”状态。

因此,通常您可以通过以下两种方式之一重新构建聚合:将其初始化为默认状态,然后向其发送一堆消息,或者使用 Factory 模式,如蓝皮书所述。

这意味着两者之间的额外映射,这使得代码更加复杂

也许,但它也确保您的域代码较少依赖 ORM 魔法。特别是,这意味着您的域逻辑可以在不同的数据结构上运行,而不是用于使持久性“轻松”的数据结构。

但它不是免费的——您必须在代码中描述如何从聚合根中获取值并返回到数据库(或作为数据库代理的 ORM 实体)。

【讨论】:

同时我想到了一种方法,我认为它与您刚才所说的相符:1)在应用程序层创建域实体将由流利的构建器完成,它创建实体并应用使用特定的流利验证器进行验证。这些构建器在 Build() 方法中生成有效实体。 2) 域实体的去实体化是通过实例化实体并直接通过 Dapper 映射属性来完成的。在很多情况下,域实体与数据库结构匹配,如果不匹配,将使用动态并将其手动映射到域实体。 @L-Four "1) 在应用层创建领域实体将由流式构建器完成,流式构建器创建实体并使用特定流式验证器应用验证。这些构建器生成有效实体,在构建()方法。” - 你为什么需要这个而不是工程噩梦??? @DmitriBodiu 我在我的问题中添加了一个示例。如您所见,这很容易,我不需要任何噩梦或过度工程,只应用标准模式:) @L-Four 我看不出它有什么价值。您可以在模型上有一个简单的 ctor。你能分享一下你的 User 模型和 UserBuilder 的实现吗? 如果你要使用构造函数,那么你最终会得到很多参数,因为它应该包含所有强制参数。构造函数也不会通过查看来明确显示业务意图。所以这些构建器只是构建域实体的助手。此外,在我的例子中,它不仅构建了一个用户(聚合根),而且还构建了非聚合根(链接帐户)的聚合,因此是一个更复杂的实体。这里解释一下:petrikainulainen.net/software-development/design/…【参考方案2】:

关键是您不使用 Dapper 来处理您的域实体,而是在您的存储库层中使用它来处理 POCO 实体。您的存储库方法将通过将 POCO 实体(Dapper 用于数据访问)转换为域实体来返回域实体。

【讨论】:

是的,这就是我描述的选项 1(动态是 POCO 实体)。但这意味着两者之间的额外映射,这使得代码更加复杂。 映射代码从不复杂,而将您的域逻辑或实体设计与数据访问问题相结合是给您的域带来不必要的复杂性和僵化的可靠方法。 其实我注意到,如果我自定义映射,例如用户映射是代码的两倍,所以在大多数情况下它更容易。在其他情况下,如果域实体与数据库不完全匹配,或者由于某种原因我需要自定义映射,我确实会使用 Dapper 的动态并将它们映射到域模型。谢谢!

以上是关于Dapper 的 DDD 原则和存储库的主要内容,如果未能解决你的问题,请参考以下文章

DDD:在存储库上删除与在实体上删除?

需要一种内置方式来为现有存储库添加死锁弹性到 Dapper 而不更改它们

DDD - 每个实体的存储库还是一个存储库?

.NET 的 SOLID DDD ORM 请求(使用干净的实体和存储库)

在 DDD 中将存储库实现保存在哪里?

对 Spring-Data DDD 存储库模式感到困惑