我将用户建模为聚合根,用户由标识符值对象和电子邮件值对象组成。两个值对象都可以唯一地标识一个用户,但是允许更改电子邮件而不能更改标识符。
在我见过的大多数 DDD 示例中,聚合根的存储库仅按标识符获取。添加另一种通过电子邮件获取到存储库的方法是否正确?我建模这么差吗?
我将用户建模为聚合根,用户由标识符值对象和电子邮件值对象组成。两个值对象都可以唯一地标识一个用户,但是允许更改电子邮件而不能更改标识符。
在我见过的大多数 DDD 示例中,聚合根的存储库仅按标识符获取。添加另一种通过电子邮件获取到存储库的方法是否正确?我建模这么差吗?
我会说是的,存储库具有通过身份以外的其他方式检索聚合的方法是合适的。但是,有一些微妙之处需要注意。
许多存储库示例仅通过 ID 检索的原因是基于这样的观察,即存储库与聚合结构相结合并不能满足所有查询要求。例如,如果您有一个查询,它调用聚合中的某些字段以及引用聚合和一些汇总数据的某些字段,则相应的聚合类不能用于表示此数据。相反,需要一个专用的读取模型。因此,查询职责与存储库分离。这有几个优点(查询可以由专门的非规范化存储提供),它是CQRS的主要范例. 在这种类型的架构中,域类仅在需要执行某些行为时才由存储库检索。所有只读用例都由读取模型提供服务。
我认为存储库具有 GetByEmail 方法的原因是基于 YAGNI 和与复杂性作斗争。您可以让您的应用程序随着需求的变化和增长而发展。您无需立即跳转到 CQRS 和单独的读/写存储。您可以从一个恰好也有查询方法的存储库开始。唯一要记住的是,当您需要在这些实体上调用某些行为时,您应该尝试按 ID 检索实体。
在我见过的大多数 DDD 示例中,聚合根的存储库仅按标识符获取。
我很想知道你看过哪些例子。根据 DDD定义,存储库是
一种封装存储、检索和搜索行为的机制,它模拟对象的集合。
搜索显然包括通过各种标准获取一个根或一组根,而不仅仅是它们的 ID。
存储库是 、 等的GetCustomerByEmail()
理想GetCustomersOver18()
场所GetCustomersByCountry(...)
。
添加另一种通过电子邮件获取到存储库的方法是否正确?- 我不会那样做。在我看来,存储库应该只有通过 id 获取、保存和删除的方法。
我宁愿问为什么您在要获取用户并在其上调用域方法的命令处理程序中没有用户 ID。我不知道你到底在做什么,但是对于登录/注册场景,我会做以下事情。当用户登录时,他传递了一个电子邮件地址和一个密码,然后您进行查询以验证用户身份 - 这不会使用域或存储库(仅用于命令),但会使用一些查询实现,该实现会返回一些UserDto 将包含用户 ID,从这一点开始,您就有了用户 ID。下一个场景是注册。创建新用户的命令处理程序将创建一个新用户实体,然后用户需要登录。
我会将此功能放入特定于您的用户对象的服务/业务层中。并非每个对象都会有一个电子邮件标识符。这似乎更像是业务逻辑,而不是存储库的责任。我相信您已经知道这一点,但是这里很好地解释了我在说什么。
我不建议这样做,但是您可以为公开a GetByEmail(string emailAddress)
方法的用户提供存储库的特定实现,但我仍然喜欢服务理念。
我同意 eulerfx 的回答:
你需要问自己为什么需要使用 ID 以外的东西来获取 AR。
我认为很明显,您没有ID,但您确实有一些其他唯一标识符,例如电子邮件地址。
如果您使用 CQRS,您需要首先确定数据对域重要还是仅对查询存储重要。如果您要求数据 100% 一致,那么它会稍微改变一些东西。例如,如果要检查电子邮件地址是否存在以满足唯一性约束,则需要 100% 的一致性。如果查询的数据在任何时候都是陈旧的,您可能会遇到问题。
请记住,存储库代表各种类型的集合。因此,如果您不需要在 AR(命令端)上实际操作,但您已经决定在哪里使用您的域是合适的,那么您可以随时ContainsEMailAddress
在存储库上进行操作;否则,您的域数据存储也可以有一个查询端,因为您的域数据存储(OLTP 类型存储)是 100% 一致的,而您的查询存储(OLAP 类型存储)可能只是最终一致,这在 CQRS 中是典型的,具有单独的查询商店。