For example, you may want to JOIN x, y, and z and display the COUNT value on the screen. You may want to aggregate the values requested by the screen from the DB and return them in the API. [^ 1]
[^ 1]: This article uses API as an example, but the same method can be applied even if it is not API.
In such a case, if the DB model is mapped to the domain model and the domain model is mapped to the API interface and returned, the following problems will be encountered.
In this article, I'll try to solve the above problem with a simple CQRS.
Simply put, CQRS is a method that separates write (Commaond) and read (Query) processes. For details, please refer to "Japanese translation of CQRS".
CQRS is often talked about with event sourcing, but it is not mandatory to introduce it with event sourcing.
In this article, the first step in CQRS is to implement command and query separation within your application. Conversely, it does not cover the following elements that appear in the more advanced CQRS:
Imagine a service like Qiita.
Consider registering Like as an example of Command and getting a list of articles as an example of Query.
When registering a Like, implement the business logic to check that it is not a Like by the poster himself. To get the article list, the title, poster name, and number of Likes are returned in the same way as Qiita top page.
The samples in this article are implemented in Spring Boot (Java). I use MyBatis as the ORM because I want to write SQL freely on the Query side.
-Practical Domain Driven Design -.NET Enterprise Application Architecture 2nd Edition -Clean Architecture Software structure and design learned from masters
With reference to the above, the configuration is as follows.
Looking at the above figure in the directory structure, it is as follows.
.
src/main/java/
└── com
└── example
└── minimumcqrssample
├── MinimumCqrsSampleApplication.java
├── application
│ ├── exception
│ └── service
├── domain
│ └── model
├── infrastructure
│ ├── mysqlquery
│ └── mysqlrepository
└── interfaces
└── api
It will be implemented from here. The code has also been uploaded to GitHub.
On the Command side, the implementation is similar to the case without CQRS.
It is implemented in 4 layers.
interfaces.api
An implementation of Controller.
LikeCommandController.java
@RestController
@RequestMapping("/articles/{articleId}/likes")
@AllArgsConstructor
public class LikeCommandController {
private LikeApplicationService service;
@PostMapping
public ResponseEntity<Void> post(@AuthenticationPrincipal SampleUserDetails sampleUserDetails,
@PathVariable long articleId) {
service.register(new ArticleId(articleId), sampleUserDetails.getUserId());
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
The required parameters are extracted from the request path, etc., and ApplicationService is called. If the request body exists, create a type like LikePostCommandRequest and bind it with @RequestBody.
When the process is complete, it returns a 201 Created HTTP response.
application.service
The application layer. This layer is responsible for realizing use cases and controlling transactions.
LikeApplicationService.java
@Service
@Transactional
@AllArgsConstructor
public class LikeApplicationService {
private LikeRepository likeRepository;
private ArticleRepository articleRepository;
public void register(ArticleId articleId, UserId userId) {
Article article = articleRepository.findById(articleId)
.orElseThrow(BadRequestException::new);
Like like = Like.of(article, userId);
likeRepository.save(like);
}
}
For type security, we receive articleId and userId as a dedicated type rather than long. Since it is implemented by the domain model pattern, the work of ApplicationService is small, and the use case is realized only by using the interface of the domain model. [^ 2]
[^ 2]: In this example, it is implemented by the domain model instead of the transaction script, but it can be replaced with the transaction script. Click here for the difference between domain model and transaction script [https://qiita.com/os1ma/items/7a229585ebdd8b7d86c2#%E3%83%93%E3%82%B8%E3%83%8D%E3%82% B9% E3% 83% AD% E3% 82% B8% E3% 83% 83% E3% 82% AF% E5% B1% A4).
In this example, the return value of ApplicationService is void, but if you want to return Location in the HTTP response, you can also return the ID from ApplicationService.
domain.model
Implement business logic in the domain model. This example implements the logic that you can't like articles you post.
The ApplicationService above deals with two aggregates, Like and Article, so let's take a look at those two.
domain.model.like
Like.java
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Setter(AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Like {
/**
* Factory.
*/
public static Like of(Article article, UserId userId) {
if (article.writtenBy(userId)) {
throw new IllegalArgumentException();
}
return new Like(article.id(), userId);
}
private ArticleId articleId;
private UserId userId;
}
I created a factory that reflects the business logic as a static method of Like, and the constructor is private. [^ 3] Also, for Article and User aggregates, we only refer to the ID of the aggregate root so that we don't directly reference other aggregates.
[^ 3]: You can cut it out to another class instead of using it as a static method.
The Like class is a root object (aggregate root) of an aggregate that is a unit of data persistence. Repository will be created for each aggregation, and a save method with the aggregation root as an argument will be prepared.
LikeRepository.java
public interface LikeRepository {
void save(Like like);
}
domain.model.article
Article.java
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Setter(AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Article {
private ArticleId id;
private UserId userId;
private String title;
public ArticleId id() {
return this.id;
}
public boolean writtenBy(UserId userId) {
return this.userId.equals(userId);
}
}
The Article class has a writtenBy method instead of Getter for userId to prevent userId from being handled from the outside.
ArticleRepository.java
public interface ArticleRepository {
Optional<Article> findById(ArticleId articleId);
}
infrastructure.repositoryimpl
An implementation of DB access.
LikeMySQLRepository.java
@Repository
@AllArgsConstructor
public class LikeMySQLRepository implements LikeRepository {
private LikeMapper likeMapper;
@Override
public void save(Like like) {
likeMapper.save(like);
}
}
LikeMapper.java
@Mapper
public interface LikeMapper {
void save(Like like);
}
LikeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.minimumcqrssample.infrastructure.mysqlrepository.like.LikeMapper">
<insert id="save" parameterType="com.example.minimumcqrssample.domain.model.like.Like">
INSERT INTO `likes` (`article_id`, `user_id`) VALUES
(#{articleId.value}, #{userId.value})
</insert>
</mapper>
Since Repository is created in aggregate units and Mapper is created in table units, MySQL Repository and Mapper have a one-to-many relationship.
This is the implementation on the Query side, which is the main part of this article.
interfaces.api
The Controller and Response types are just implemented normally.
ArticleQueryController.java
@RestController
@RequestMapping("/articles")
@AllArgsConstructor
public class ArticleQueryController {
private ArticleQueryService service;
@GetMapping
public ResponseEntity<ArticleListQueryResponse> list() {
return ResponseEntity.ok(service.list());
}
}
ArticleListQueryResponse.java
@Data
@AllArgsConstructor
public class ArticleListQueryResponse {
private List<Article> articles;
@Data
@AllArgsConstructor
public static class Article {
private String title;
private String authorName;
private long likeCount;
}
}
By incorporating CQRS, we are creating an interface called QueryService.
ArticleQueryService.java
public interface ArticleQueryService {
ArticleListQueryResponse list();
}
The QueryService interface seems to be better placed in the application layer, but in this example it is placed in the interface layer. The reason is as follows.
If you want to achieve more complicated processing, you may want to place it in the application layer.
In addition, the article "Returning to the basics of design that failed and domain-driven design" For applications where the Query side is important, you may also need a domain layer for Query.
infrastructure.queryimpl
Finally, the implementation of Query.
LikeMySQLRepository.java
@Service
@AllArgsConstructor
public class ArticleMySQLQueryService implements ArticleQueryService {
private ArticleMySQLQueryMapper mapper;
@Override
public ArticleListQueryResponse list() {
return new ArticleListQueryResponse(mapper.list());
}
}
ArticleMySQLQueryMapper.java
@Mapper
public interface ArticleMySQLQueryMapper {
List<ArticleListQueryResponse.Article> list();
}
In this example, Repository and Mapper are separated, but it is safe to integrate them.
ArticleMySQLQueryMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.minimumcqrssample.infrastructure.mysqlquery.article.ArticleMySQLQueryMapper">
<resultMap id="article"
type="com.example.minimumcqrssample.interfaces.api.article.ArticleListQueryResponse$Article">
<result property="title" column="title"/>
<result property="authorName" column="author_name"/>
<result property="likeCount" column="like_count"/>
</resultMap>
<select id="list" resultMap="article">
SELECT
MAX(a.title) AS title,
MAX(u.name) AS author_name,
COUNT(*) AS like_count
FROM articles a
INNER JOIN users u ON a.user_id = u.id
INNER JOIN likes l ON a.id = l.article_id
GROUP BY l.article_id
</select>
</mapper>
In SQL, JOIN, COUNT, etc. are freely described.
This is what I mentioned at the beginning of this article
The problem has been solved.
I think simple CQRS is a pretty good solution to the motivation to write SQL freely in a reference system. It seems that you will not have to worry when you are told "I want to display ~~ on the screen".
On the other hand, in the update system, writing a monotonous SELECT statement or INSERT statement is only a hassle. If you just need methods like findById and save, JPA may be a better match than MyBatis. "[DDD x CQRS-A story that worked well with different ORMs in the update and reference systems](https://speakerdeck.com/littlehands/ddd-x-cqrs-geng-xin-xi-tocan-zhao- As introduced in "xi-teyi-naruormwobing-yong-siteshang-shou-kuitutahua)", it seems quite good to change the ORM between the update system and the reference system.
-Practical Domain Driven Design -.NET Enterprise Application Architecture 2nd Edition -Clean Architecture Software structure and design learned from masters
Web
Recommended Posts