Implemented a strong API for "I want to display ~~ on the screen" with simple CQRS

Introduction

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.

What is 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:

Implementation theme

Imagine a service like Qiita.

CQRS_ドメインモデル (3).png

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.

Language to use, FW, etc.

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.

Constitution

-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.

CQRS (2).png

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

Implementation

It will be implemented from here. The code has also been uploaded to GitHub.

Command side

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.

Query side

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.

in conclusion

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.

reference

Books

-Practical Domain Driven Design -.NET Enterprise Application Architecture 2nd Edition -Clean Architecture Software structure and design learned from masters

Web

Recommended Posts

Implemented a strong API for "I want to display ~~ on the screen" with simple CQRS
I want to create a chat screen for the Swift chat app!
I want to hit the API with Rails on multiple docker-composes set up locally
I want to use screen sharing on the login screen on Ubuntu 18
I want to add a browsing function with ruby on rails
I want to return to the previous screen with kotlin and java!
I want to display background-ground-image on heroku.
I want to display the number of orders for today using datetime.
I want to display a PDF in Chinese (Korean) with thin reports
[For beginners] I want to automatically enter pre-registered data in the input form with a selection command.
I want to download a file on the Internet using Ruby and save it locally (with caution)
I want to play a GIF image on the Andorid app (Java, Kotlin)
I want to display the name of the poster of the comment
I want to dark mode with the SWT app
I want to monitor a specific file with WatchService
I want to simplify the log output on Android
I want to create a generic annotation for a type
I want to add a delete function to the comment function
Rspec: I want to test the post-execution state when I set a method on subject
I want to go back to a specific VC by tapping the back button on the NavigationBar!
I want to place RadioButtons in the same RadioGroup at any position on the screen.
[Rails] I want to display the link destination of link_to in a separate tab
I want to make a list with kotlin and java!
I want to call a method and count the number
I want to make a function with kotlin and java!
I want to use the Java 8 DateTime API slowly (now)
I want to create a form to select the [Rails] category
What I was addicted to with the Redmine REST API
Even in Java, I want to output true with a == 1 && a == 2 && a == 3
I want to put the JDK on my Mac PC
I want to give a class name to the select attribute
I want to distinct the duplicated data with has_many through
I want to return a type different from the input element with Java8 StreamAPI reduce ()
I want to transition to the same screen in the saved state
I want to narrow down the display of docker ps
I want to use FireBase to display a timeline like Twitter
I want to return multiple return values for the input argument
I want to pass the startup command to postgres with docker-compose.
How to deal with the type that I thought about writing a Java program for 2 years
I want to recursively search for files under a specific directory
I want to make a button with a line break with link_to [Note]
I want to connect SONY headphones WH-1000XM4 with LDAC on ubuntu 20.04! !!
The story of Collectors.groupingBy that I want to keep for posterity
Logic to draw a circle with ASCII art on the console
How to display products by category on the same list screen
I made a simple search form with Spring Boot + GitHub Search API.
I want to add the disabled option to f.radio_button depending on the condition
I want to display the images under assets/images in the production environment
[Java] I want to perform distinct with the key in the object
[Introduction to JSP + Servlet] I played with it for a while ♬
I tried to display the calendar on the Eclipse console using Java.
I want to extract between character strings with a regular expression
I want to create a Servlet war file with OpenJDK on CentOS7. Without mvn. With no internet connection.
A story that I was addicted to twice with the automatic startup setting of Tomcat 8 on CentOS 8
A story that I wanted to write a process equivalent to a while statement with the Stream API of Java8
A story I was addicted to when testing the API using MockMVC
I want to display images with REST Controller of Java and Spring!
I want to select multiple items with a custom layout in Dialog
Even in Java, I want to output true with a == 1 && a == 2 && a == 3 (PowerMockito edition)
Practice of Java programming basics-I want to display triangles with for statements ①
I tried to make a Web API that connects to DB with Quarkus