这里的情况让我很困惑。
我有两张表:users
和articles
。一个用户可以写多篇文章,一篇文章只能有一个作者。从这个行业。我有两个实体:
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)
这样的数据。CCD_ 5对于将CCD_ 6映射到包括CCD_ 8的CCD_。
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中聚合之间的无引用约束,这应该是这样的:
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());
}
}
那么应该使用哪一个呢?或者还有更好的主意吗?
问题
写入/命令&read/query需求是正交的,这些需求将模型拉向相反的方向,这会在统一的模型中产生张力,并可能(而且经常)导致巨大的混乱。
命令
一方面,您希望聚合根(AR)非常关注行为,并且只拥有以强一致的方式强制执行不变量所需的最少数据量。这使得模型易于测试、可扩展、并发友好,并使您能够立即识别哪些数据是事务边界的一部分。当AR被正确建模时,命令通常将涉及每个事务的单个AR。
查询
另一方面,查询往往需要跨多个AR提取数据,这鼓励定义每个&每个关系作为域模型中的对象引用。这完全违背了我们指挥部的目标。然后,我们留下了一个模型,我们需要用延迟加载进行优化,这个模型在保存对象时对哪些数据被持久化非常不透明(必须检查级联配置),这更难为测试设置,它引入了AR之间的直接耦合,等等。
解决方案?CQRS
这个解决方案实际上非常简单,类似于为什么我们有有限的上下文。与其试图让一个模型实现不同的目标,我们可以有两个模型:命令模型&查询模型。
这通常被称为命令查询责任分离(CQRS)。在最复杂和优化的形式,CQRS可能意味着拥有一个完全不同的数据库(即使是实物)来处理读取,允许优化读取而不是写入的索引,去规范化数据以避免连接,等等。
幸运的是,对于大多数系统,您实际上不需要这样的可扩展性(和复杂性),并且可以通过逻辑读/写隔离使用更简单的方法来实现CQRS。实际上,这通常意味着拥有两组服务或处理程序命令服务/处理程序和查询服务/处理器。
例如,您可能具有CCD_ 14和CCD_;查询。
命令服务通常会从存储库加载AR,在这些AR上执行命令并将其保存回,而查询则可以自由使用任何实用的方法来收集数据。有时这意味着利用存储库并在应用程序级别聚合数据,有时意味着执行原始SQL,利用特定于数据库的功能,并完全传递域模型。
关键是,通过拆分非常简单的命令/查询服务,您可以专注于优化写/命令的域模型,然后在不污染命令处理流的情况下采用任何您想要满足查询需求的数据策略。查询服务往往需要一系列不同的依赖关系,并且通常会与基础设施耦合得更多,这不是命令所需要的,但对查询来说是一种非常好的折衷。
在实践中有很多这样的轻量级CQRS实现的例子,但您可以在GitHub上查看实现领域驱动设计(IDDD)协作的BC代码的应用层。
挑战
尽管我说得很简单,但你仍然最有可能面临挑战。例如,命令的不同模型&查询意味着您不能很容易地为这两种情况重用查询对象规范,命令&查询。如果您过去将授权规则建模为AR规范,那么现在可能需要在查询端复制这些规则,或者编写自定义翻译器(例如规范到SQL)。
面临的另一个常见挑战是映射复杂的专门层次结构。例如,您可能有一个案例管理系统,其中有数百种不同的案例专门化,它们有自己的模式。手动创建查询以加载数据并有效地映射这些图可能会很乏味。出于这个原因,有时我会使用专用的查询实体(而不是域模型)来映射对象关系,并让ORM来完成工作。
有时,您甚至可以将JSON存储在数据库中,并利用数据库的JSON索引功能来处理查询等。
特别是在Spring的上下文中,您可能需要额外的样板来将Pageable
与手工查询集成,甚至需要使用QueryDSL编写的JPAQuery
。
正如您所看到的,处理查询并不是一种一刀切的策略,这很好,因为这是在一个不同的逻辑模型中仔细抽象出来的,在那里您可以做任何有效的事情。
结论
你无法想象我有多频繁地在2分钟内编写一个查询(并且已经),并在DTO中手动映射它,相反,我深深地陷入了通过带有糟糕注释的Spring Data使其有效工作的困境,最终得到了一个次优且过于复杂的解决方案。
在同类数据模型中,查询也更容易处理。有没有尝试过在层次结构的根不拥有所需数据的情况下查询专用类型?这对于ORM来说是非常不切实际的。
无论如何,根据我的经验,轻量级CQRS总是比通过域模型运行查询要好,尽管它可能会带来新的挑战
- 引入DTO并返回它们是非常正确的方法
- @ManyToOne获取类型可能也应该更改为LAZY
- 这允许使用具有多个文章DTO(有用户和没有用户)
- 如果没有调用article.getUser(),就不会有额外的调用