Apply CQRS to a Spring REST API – 将CQRS应用于Spring REST API

最后修改: 2015年 9月 10日

1. Overview

1.概述

In this quick article, we’re going to do something new. We’re going to evolve an existing REST Spring API and make it use Command Query Responsibility Segregation – CQRS.

在这篇快速文章中,我们将做一些新的事情。我们将对现有的REST Spring API进行改进,使其使用命令查询责任隔离–CQRS

The goal is to clearly separate both the service and the controller layers to deal with Reads – Queries and Writes – Commands coming into the system separately.

我们的目标是明确分离服务层和控制器层,以分别处理进入系统的读-查询和写-命令。

Keep in mind that this is just an early first step towards this kind of architecture, not “an arrival point”. That being said – I’m excited about this one.

请记住,这只是迈向这种架构的早期第一步,而不是 “一个到达点”。既然如此–我对这个项目感到兴奋。

Finally – the example API we’re going to be using is publishing User resources and is part of our ongoing Reddit app case study to exemplify how this works – but of course, any API will do.

最后–我们将要使用的示例API是发布用户资源,是我们正在进行的Reddit应用案例研究的一部分,以示范如何工作–但当然,任何API都可以。

2. The Service Layer

2.服务层

We’ll start simple – by just identifying the read and the write operations in our previous User service – and we’ll split that into 2 separate services – UserQueryService and UserCommandService:

我们将从简单的开始–只确定我们之前的User服务中的读和写操作–我们将把它分成两个独立的服务–UserQueryServiceUserCommandService

public interface IUserQueryService {

    List<User> getUsersList(int page, int size, String sortDir, String sort);

    String checkPasswordResetToken(long userId, String token);

    String checkConfirmRegistrationToken(String token);

    long countAllUsers();

}
public interface IUserCommandService {

    void registerNewUser(String username, String email, String password, String appUrl);

    void updateUserPassword(User user, String password, String oldPassword);

    void changeUserPassword(User user, String password);

    void resetPassword(String email, String appUrl);

    void createVerificationTokenForUser(User user, String token);

    void updateUser(User user);

}

From reading this API you can clearly see how the query service is doing all the reading and the command service isn’t reading any data – all void returns.

通过阅读这个API,你可以清楚地看到查询服务是如何进行所有的读取工作的,而命令服务并没有读取任何数据–所有的无效返回

3. The Controller Layer

3.控制器层

Next up – the controller layer.

接下来是–控制器层。

3.1. The Query Controller

3.1.查询控制器

Here is our UserQueryRestController:

这里是我们的UserQueryRestController

@Controller
@RequestMapping(value = "/api/users")
public class UserQueryRestController {

    @Autowired
    private IUserQueryService userService;

    @Autowired
    private IScheduledPostQueryService scheduledPostService;

    @Autowired
    private ModelMapper modelMapper;

    @PreAuthorize("hasRole('USER_READ_PRIVILEGE')")
    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public List<UserQueryDto> getUsersList(...) {
        PagingInfo pagingInfo = new PagingInfo(page, size, userService.countAllUsers());
        response.addHeader("PAGING_INFO", pagingInfo.toString());
        
        List<User> users = userService.getUsersList(page, size, sortDir, sort);
        return users.stream().map(
          user -> convertUserEntityToDto(user)).collect(Collectors.toList());
    }

    private UserQueryDto convertUserEntityToDto(User user) {
        UserQueryDto dto = modelMapper.map(user, UserQueryDto.class);
        dto.setScheduledPostsCount(scheduledPostService.countScheduledPostsByUser(user));
        return dto;
    }
}

What’s interesting here is that the query controller is only injecting query services.

这里有趣的是,查询控制器只注入查询服务。

What would be even more interesting is to cut off the access of this controller to the command services – by placing these in a separate module.

更有趣的是,切断该控制器对命令服务的访问–通过将这些放在一个单独的模块中。

3.2. The Command Controller

3.2.指挥控制器

Now, here’s our command controller implementation:

现在,这是我们的命令控制器的实现。

@Controller
@RequestMapping(value = "/api/users")
public class UserCommandRestController {

    @Autowired
    private IUserCommandService userService;

    @Autowired
    private ModelMapper modelMapper;

    @RequestMapping(value = "/registration", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void register(
      HttpServletRequest request, @RequestBody UserRegisterCommandDto userDto) {
        String appUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
        
        userService.registerNewUser(
          userDto.getUsername(), userDto.getEmail(), userDto.getPassword(), appUrl);
    }

    @PreAuthorize("isAuthenticated()")
    @RequestMapping(value = "/password", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void updateUserPassword(@RequestBody UserUpdatePasswordCommandDto userDto) {
        userService.updateUserPassword(
          getCurrentUser(), userDto.getPassword(), userDto.getOldPassword());
    }

    @RequestMapping(value = "/passwordReset", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void createAResetPassword(
      HttpServletRequest request, 
      @RequestBody UserTriggerResetPasswordCommandDto userDto) 
    {
        String appUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
        userService.resetPassword(userDto.getEmail(), appUrl);
    }

    @RequestMapping(value = "/password", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void changeUserPassword(@RequestBody UserchangePasswordCommandDto userDto) {
        userService.changeUserPassword(getCurrentUser(), userDto.getPassword());
    }

    @PreAuthorize("hasRole('USER_WRITE_PRIVILEGE')")
    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void updateUser(@RequestBody UserUpdateCommandDto userDto) {
        userService.updateUser(convertToEntity(userDto));
    }

    private User convertToEntity(UserUpdateCommandDto userDto) {
        return modelMapper.map(userDto, User.class);
    }
}

A few interesting things are happening here. First – notice how each of these API implementations is using a different command. This is mainly to give us a good base for further improving the design of the API and extracting different resources as they emerge.

这里发生了一些有趣的事情。首先–注意到这些API实现中的每一个是如何使用不同的命令的。这主要是为了给我们提供一个良好的基础,以便进一步改进API的设计,并在不同的资源出现时提取它们。

Another reason is that when we take the next step, towards Event Sourcing – we have a clean set of commands that we’re working with.

另一个原因是,当我们采取下一步行动,走向事件采购时,我们有一套干净的命令,我们正在使用。

3.3. Separate Resource Representations

3.3.独立的资源代表

Let’s now quickly go over the different representations of our User resource, after this separation into commands and queries:

现在让我们快速浏览一下我们的用户资源的不同表现形式,在这种分离为命令和查询之后。

public class UserQueryDto {
    private Long id;

    private String username;

    private boolean enabled;

    private Set<Role> roles;

    private long scheduledPostsCount;
}

Here are our Command DTOs:

下面是我们的命令DTO。

  • UserRegisterCommandDto used to represent user registration data:
public class UserRegisterCommandDto {
    private String username;
    private String email;
    private String password;
}
  • UserUpdatePasswordCommandDto used to represent data to update current user password:
public class UserUpdatePasswordCommandDto {
    private String oldPassword;
    private String password;
}
  • UserTriggerResetPasswordCommandDto used to represent user’s email to trigger reset password by sending an email with reset password token:
public class UserTriggerResetPasswordCommandDto {
    private String email;
}
  • UserChangePasswordCommandDto used to represent new user password – this command is called after user use password reset token.
public class UserChangePasswordCommandDto {
    private String password;
}
  • UserUpdateCommandDto used to represent new user’s data after modifications:
public class UserUpdateCommandDto {
    private Long id;

    private boolean enabled;

    private Set<Role> roles;
}

4. Conclusion

4.结论

In this tutorial, we laid the groundwork towards a clean CQRS implementation for a Spring REST API.

在本教程中,我们为Spring REST API的一个干净的CQRS实现奠定了基础。

The next step will be to keep improving the API by identifying some separate responsibilities (and Resources) out into their own services so that we more closely align with a Resource-centric architecture.

下一步将是继续改进API,将一些单独的责任(和资源)确定为他们自己的服务,以便我们更紧密地与以资源为中心的架构保持一致。