DDD:选择与JPA/Hibernate的关系或仅ID参考
这是一种情况,我很困惑。
我有两个表:用户
和文章
。一个用户可以写多篇文章,一篇文章只能有一个作者。从这个业务。 I have two entity:
class User {
long id;
String username;
}
class Article {
long id;
String title;
String content;
}
If I follow the JPA style, the Article
should be like this:
class Article {
long id;
String title;
String content;
@ManyToOne
User author;
}
This will make the query service quite easy.例如,我可能有一个查询服务来获取fetchNewestarticleswithuserinfo(页面)
之类的数据。 @manytoone
对于文章
映射到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
实体将来变得越来越复杂,则使用
作者
不必要。
If I follow the no reference between aggregates constraint in DDD, this should look like this:
class Article {
long id;
String title;
String content;
@ManyToOne
long authorId;
}
This makes the Article
looks clean (in my opinion) and easy to build even the 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());
}
}
那么应该使用哪一个?还是有更好的主意?
Here is a situation makes me quite confusing.
I have two tables: users
and articles
. One user can write multiple articles and one article can only have one author. From this business. I have two entity:
class User {
long id;
String username;
}
class Article {
long id;
String title;
String content;
}
If I follow the JPA style, the Article
should be like this:
class Article {
long id;
String title;
String content;
@ManyToOne
User author;
}
This will make the query service quite easy. For example, I may have a query service to get data like fetchNewestArticlesWithUserInfo(Page page)
. The @ManyToOne
is quite useful for mapping the Article
into ArticleDTO
including 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);
}
But if the User
entity getting more and more complex in the future, fetching Article
with author
(eager fetch by default for @ManyToOne) seems quite unnecessary.
If I follow the no reference between aggregates constraint in DDD, this should look like this:
class Article {
long id;
String title;
String content;
@ManyToOne
long authorId;
}
This makes the Article
looks clean (in my opinion) and easy to build even the User
if more complex. But it makes the query service quite hard to implement. You will lose the benifit of the relationship in JPQL and have to write code for DTO assembling.
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());
}
}
So which one should be used? Or is there any better idea?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
问题
写入/命令&amp; 阅读/查询需求是正交的,而这些需求是将模型朝相反的方向拉动,从而在统一模型中造成了张力,并且可以(通常)会导致巨大的混乱。
一方面
,您希望汇总根(ARS)非常注重行为,并且只拥有以强烈的方式执行不变性所需的最小数据。这使得一个易于测试的模型,可扩展,对并发友好,并让您立即确定哪些数据是交易边界的一部分。正确建模AR时,命令通常将涉及每个事务的单个AR。
;
另一方面,查询往往需要跨多个AR提取数据,这鼓励定义每个ARP 每个关系作为域模型中的对象引用。这完全违背了我们的指挥副目标。然后,我们有一个模型,需要使用懒惰加载来优化,该模型在保存对象时(必须检查Cascade配置)在哪些数据中持续存在非常不透明在AR等之间。
解决方案? CQRS!
该解决方案实际上非常简单,类似于我们为何有界情况。与其尝试实现不同的目标,我们可以有两个模型:命令模型&amp; 查询模型。
通常称为命令查询责任egrication(cqrs)。最复杂,
优化的表单,CQR可能意味着具有完全不同的数据库(甚至是类型)来处理读取,从而优化读取而不是写入索引,而不是写入数据,以避免加入等级等
。这种可扩展性(和复杂性)可以通过具有逻辑读/写隔离来使用更简单的方法来实现CQR。实际上,通常意味着拥有两组服务或处理人员。 命令服务/处理程序和查询服务/处理程序。
例如,您可以具有
commandorderService
和QueryorDerService
来处理命令&amp;查询分别。虽然命令服务通常会从存储库中加载AR,请在这些服务库中执行命令并保存它们,但 Query 服务将可以免费使用任何实用手段来收集数据。有时,这意味着要利用存储库并在应用程序级别汇总数据,有时意味着执行原始SQL,利用数据库特定的功能并完全通过域模型进行操作。
关键是,通过将非常简单的命令/查询服务拆分,您可以专注于优化写入/命令的域模型,然后在不污染命令处理流量的情况下满足查询需求的任何数据策略。查询服务倾向于需要一系列不同的依赖项,并且通常会更加与基础架构相结合,这不是您想要的命令,而是查询的完全很好的权衡。
实际上,有许多此类轻型CQRS实现的示例,但是您可以查看实现域驱动设计(IDDD)协作的应用层的BC github上的代码。
挑战
即使我听起来很简单,您仍然最有可能面临挑战。例如,命令的不同模型&amp;查询意味着您无法轻松使用这两个,命令&amp;的查询对象规范。查询。如果您曾经将授权规则建模为AR规范,则现在可能必须在查询端复制这些规则或编写自定义转换器(例如SQL SQL)。
面对的另一个普遍挑战是绘制复杂的专业层次结构。例如,您可能会有一个案例管理系统,其中有数百种不同的案例专业案例。手动制作查询以加载数据,它们有效地绘制这些图可能很乏味。因此,有时我使用专用查询实体(不是域模型),其中我映射对象关系并让ORM完成工作。
有时,您甚至可以将DB的JSON存储在DB中,并利用DB的JSON索引功能来处理查询等。
在Spring的背景下,您可能需要其他样板来集成
Pagable-capeable
进行了查询,甚至jpaquery
用查询DSL编写。如您所见,没有一个尺寸适合处理查询的所有策略,这很好,因为这是在可以做任何有效的任何逻辑模型中精心抽象的。
结论
您无法想象我可以在2分钟内写出查询的频率(并进行)并在DTO A中手动映射,而不是深深地吸引它通过春季数据,并带有可怕的注释,并最终得到一个子。 - 最佳且过于复杂的解决方案。
查询也更容易查看均匀数据模型。曾经尝试查询专业类型,层次结构的根部没有您需要的数据? ORMS非常不切实际。
无论如何,以我的经验,轻量级CQR总是比通过域模型进行的运行查询要好,尽管可能带来了新的挑战。
The problem
The write/command & read/query needs are orthogonal and those are pulling the model in opposite directions which creates tension in a unified model and can (and often does) lead to a huge mess.
Commands
On the one hand, you want aggregate roots (ARs) to be very behavior-focused and only own the minimal amount of data necessary to enforce invariants in a strongly-consistent way. That makes for a model that is easy to test, that's scalable, concurrency-friendly and lets you immediately identify which data is part of the transactional boundary. When ARs are properly modeled the commands will generally involve a single AR per transaction.
Queries
On the other hand, queries tends to need to pull data across multiple ARs, which encourages defining each & every relationship as object references in the domain model. That totally works against our command side goal. We are then left with a model where we need to optimize with lazy loading, a model that's quite opaque regarding which data is getting persisted when saving an object (have to check cascade configs), that's harder to setup for tests, that introduces direct coupling between ARs, etc.
The solution? CQRS!
The solution is actually very simple and akin to why we have bounded contexts. Rather than attempting to make a single model fulfill different goals we can have two models: the command model & the query model.
That's generally referred to as Command Query Responsibility Segregation (CQRS). In it's most complex and
optimized form, CQRS could mean having an entirely different database (even in kind) to process reads, allowing to optimize indexes for reads rather than writes, de-normalize data to avoid joins, etc.
Fortunately, for most systems you actually do not need such scalability (and complexity) and can implement CQRS using a much more simplistic approach by having a logical read/write segregation. In practical terms that generally just means having two sets of services or handlers. Command services/handlers and Query services/handlers.
e.g. you may have a
CommandOrderService
and aQueryOrderService
to process commands & queries respectively.While the command services would usually load ARs from repositories, execute commands on those and save them back, the query services would be free to use any practical means to gather the data. Sometimes that means leveraging repositories and aggregating data at the application level, sometimes it means executing raw SQL, leveraging database-specific features and by-passing the domain model entirely.
The point is, by having that very simple command/query service split then you can focus on optimizing the domain model for writes/commands and then resort on any data strategy you would like to fulfill query needs without polluting your command processing flows. Query services tends to require a range of different dependencies and will often be much more coupled to the infrastructure, which isn't something you'd want for commands, but is a perfectly fine trade-off for queries.
There's many examples of such lightweight CQRS implementation in practice, but you can have a look at the application layer of the Implementing Domain-Driven Design (IDDD) Collaboration's BC code on GitHub.
Challenges
Even though I made it sound so simple, you are still most likely to face challenges. For instance, a different model for commands & queries means you can't easily re-use query object specifications for both, commands & queries. If you used to model authorization rules as AR specifications, you now might have to duplicate those rules on the query side or write custom translators (e.g. spec to SQL).
Another common challenge to face is to map complex specialized hierarchies. For instance, you might have a case management system where there's hundreds of different case specializations with their own schema. Manually crafting queries to load data and them map those graphs effectively could be tedious. For that reason sometimes I use dedicated query entities (not the domain model) where I map object relationships and let the ORM do the work.
Sometimes, you may even store JSON in the DB and leverage JSON-indexing features of your DB to process queries, etc.
In the context of Spring specifically, you could need additional boilerplate to integrate
Pageable
with hand-made queries or evenJPAQuery
written with Query DSL.As you can see there's not a one size fits all strategy to process queries and that's fine because that's carefully abstracted away in a different logical model where you can do whatever works.
Conclusion
You can't imagine how often I could have written a query in 2 minutes (and have) and map it manually in a DTO an instead got drawn deep into making it work forcefully through Spring Data with awful annotations and end up with a sub-optimal and overly complex solution.
Queries are also so much easier to process looking at an homogeneous data model. Ever tried to query specialized types where the root of the hierarchy didn't own the data you need? It's very impractical with ORMs.
Anyway, in my experience lightweight CQRS always was better than the running queries through the domain model despite the new challenges that could comes with it.