DDD:选择与 JPA/Hibernate 的关系或仅 id 引用

DDD: choose relationship or only id reference with JPA/Hibernate

这里有一个情况让我很困惑。

我有两个表:usersarticles。一个用户可以写多篇文章,一篇文章只能有一个作者。从这个业务。我有两个实体:

class User {
  long id;
  String username;
}

class Article {
  long id;
  String title;
  String content;
}

如果我遵循 JPA 风格,Article 应该是这样的:

class Article {
  long id;
  String title;
  String content;
  @ManyToOne
  User author;
}

这将使查询服务变得非常容易。例如,我可能有一个查询服务来获取像 fetchNewestArticlesWithUserInfo(Page page) 这样的数据。 @ManyToOne 对于将 Article 映射到 ArticleDTO 包括 UserDTO.

非常有用
interface ArticleDTO {
  long getId();
  String getTitle();
  UserDTO getAuthor();
}

interface UserDTO {
  long getId();
  String getUsername();
}

interface ArticleRepository {
  @Query("select a from Article a left join fetch a.author")
  Page<ArticleDTO> fetchNewestArticlesWithUserInfo(PageRequest page);
}

但是,如果 User 实体在未来变得越来越复杂,那么使用 author 获取 Article(@ManyToOne 默认情况下是预先获取)似乎是完全没有必要的。

如果我遵循 DDD 中的 no reference between aggregates 约束,这应该是这样的:

class Article {
  long id;
  String title;
  String content;
  @ManyToOne
  long authorId;
}

这使得 Article 看起来很干净(在我看来)并且即使更复杂的 User 也很容易构建。但这使得查询服务很难实现。你将失去JPQL中关系的好处,必须编写DTO组装代码。

class ArticleQueryService {
  private ArticleRepository articleRepository;
  private UserRepository userRepository;

  Page<ArticleDTO> fetchNewestArticlesWithUserInfo(PageRequest page) {
    Page<Article> articles = articleRepository.fetchArticles(page);
    Map<Long, UserDTO> users = userRepository.findByIds(articles.stream().map(Article::getAuthorId).collect(toList()))
      .stream().collect(toMap(u => u.getId(), u => u));
    return articles.stream().map(a => return new ArticleDTO(a.getId(), a.getTitle(), users.get(a.getAuthorId()))).collect(toList());
  }
}

那么应该使用哪一个呢?或者有什么更好的办法吗?

问题

write/command & read/query 的需求是正交的,它们将模型拉向相反的方向在统一模型中造成紧张,并可能(而且经常)导致大混乱。

命令

一方面,您希望聚合根 (AR) 非常 behavior-focused 并且只拥有以 strongly-consistent 方式强制执行不变量所需的最少数据量。这使得模型易于测试、可扩展、concurrency-friendly 并让您立即识别哪些数据是事务边界的一部分。当 AR 被正确建模时,命令通常会涉及每个事务的单个 AR。

查询

另一方面,查询往往需要跨多个 AR 提取数据,这鼓励将每个关系定义为域模型中的对象引用。这完全违背了我们的指挥方面的目标。然后我们留下一个模型,我们需要通过延迟加载进行优化,这个模型对于保存对象时哪些数据被持久化是非常不透明的(必须检查级联配置),很难为测试设置,它引入了直接耦合AR之间等

解决办法? CQRS!

解决方案实际上非常简单,类似于为什么我们有限界上下文。我们可以有两个模型,而不是试图让一个模型实现不同的目标:command 模型和 query 模型。

一般称为Command Query Responsibility Segregation (CQRS)。在它最复杂和 优化形式,CQRS 可能意味着有一个完全不同的数据库(甚至是同类数据库)来处理读取,允许优化读取而不是写入的索引,de-normalize 数据以避免连接等。

幸运的是,对于大多数系统,您实际上不需要这样的可伸缩性(和复杂性),并且可以通过 逻辑 read/write 隔离[=73= 使用更简单的方法来实现 CQRS ].实际上,这通常意味着拥有两组服务或处理程序。 命令services/handlers查询services/handlers.

例如你可能有一个 CommandOrderService 和一个 QueryOrderService 来分别处理命令和查询。

虽然 command 服务通常会从存储库加载 AR,对这些执行命令并将其保存回来,但 query 服务将是自由使用任何实用的方法来收集数据。有时这意味着在应用程序级别利用存储库和聚合数据,有时这意味着执行原始 SQL,充分利用 database-specific 功能和 by-passing 域模型。

重点是,通过非常简单的 command/query 服务拆分,您可以专注于优化 writes/commands 的域模型,然后采用您想要满足查询需求的任何数据策略而不会污染您的命令处理流程。查询服务往往需要一系列不同的依赖项,并且通常会更加耦合到基础设施,这不是您想要的命令,但对于查询来说非常好trade-off。

在实践中有很多这样的轻量级 CQRS 实现的例子,但是你可以看看 Implementing Domain-Driven Design (IDDD) Collaboration's BC code on GitHub.[=16= 的应用层]

挑战

尽管我说的很简单,但您仍然很可能面临挑战。例如,命令和查询的不同模型意味着您不能轻松地 re-use 查询命令和查询的对象规范。如果您曾经将授权规则建模为 AR 规范,那么您现在可能必须在查询端复制这些规则或编写自定义转换器(例如规范 SQL)。

面临的另一个常见挑战是映射复杂的专业层次结构。例如,您可能有一个案例管理系统,其中有数百个不同的案例专业化,它们都有自己的模式。手动制作查询以加载数据并有效地映射这些图表可能很乏味。出于这个原因,有时我会使用专用的查询实体(而不是域模型)来映射对象关系并让 ORM 完成工作。

有时,您甚至可以将 JSON 存储在数据库中,并利用数据库的 JSON-indexing 功能来处理查询等。

特别是在 Spring 的上下文中,您可能需要额外的样板来将 Pageable 与 hand-made 查询集成,甚至 JPAQuery 使用查询 DSL 编写。

如您所见,没有一种放之四海而皆准的策略来处理查询,这很好,因为它在不同的逻辑模型中被仔细地抽象出来,您可以在其中做任何工作。

结论

您无法想象我有多少次可以在 2 分钟内(并且已经)编写一个查询并将其手动映射到 DTO 中,而是深入研究通过 Spring 数据使其有效地工作糟糕的注释和结束你sub-optimal 和过于复杂的解决方案。

查看同类数据模型,查询也更容易处理。是否曾经尝试查询层次结构的根不拥有您需要的数据的专用类型?使用 ORM 非常不切实际。

无论如何,根据我的经验,轻量级 CQRS 总是比 运行 通过领域模型的查询更好,尽管它可能带来新的挑战。

  • 引入 DTO 和 return 它们
  • 是非常正确的方法
  • @ManyToOne 获取类型可能而且应该更改为 LAZY
  • 这允许使用多个文章 DTO(有和没有用户)
  • 如果 article.getUser() 未被调用,则不会额外调用