Перейти к основному содержимому

Применение CQRS к Spring REST API

· 4 мин. чтения

1. Обзор

В этой быстрой статье мы собираемся сделать что-то новое. Мы собираемся развить существующий API REST Spring и заставить его использовать разделение ответственности команд и запросов — CQRS .

Цель состоит в том, чтобы четко разделить уровни службы и контроллера , чтобы отдельно обрабатывать операции чтения — запросы и записи — команды, поступающие в систему.

Имейте в виду, что это всего лишь первый шаг к такой архитектуре, а не «точка прибытия». При этом - я взволнован об этом.

Наконец, пример API, который мы собираемся использовать, публикует пользовательские ресурсы и является частью нашего текущего тематического исследования приложения Reddit, чтобы проиллюстрировать, как это работает, но, конечно, подойдет любой API.

2. Сервисный уровень

Мы начнем с простого — просто идентифицируя операции чтения и записи в нашем предыдущем сервисе User — и мы разделим его на 2 отдельных сервиса — UserQueryService и UserCommandService :

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);

}

Прочитав этот API, вы можете ясно увидеть, что служба запросов выполняет все операции чтения, а служба команд не считывает никаких данных — все возвращается void .

3. Уровень контроллера

Далее — уровень контроллера.

3.1. Контроллер запросов

Вот наш 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;
}
}

Что здесь интересно, так это то, что контроллер запросов внедряет только службы запросов.

Что было бы еще интереснее, так это отрезать этому контроллеру доступ к службам команд , поместив их в отдельный модуль.

3.2. Командный контроллер

Теперь вот наша реализация командного контроллера:

@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);
}
}

Здесь происходит несколько интересных вещей. Во-первых, обратите внимание, что каждая из этих реализаций API использует разные команды. В основном это дает нам хорошую основу для дальнейшего улучшения дизайна API и извлечения различных ресурсов по мере их появления.

Другая причина заключается в том, что когда мы делаем следующий шаг к Event Sourcing, у нас есть чистый набор команд, с которыми мы работаем.

3.3. Отдельные представления ресурсов

Давайте теперь быстро пробежимся по различным представлениям нашего пользовательского ресурса после разделения на команды и запросы:

public class UserQueryDto {
private Long id;

private String username;

private boolean enabled;

private Set<Role> roles;

private long scheduledPostsCount;
}

Вот наши команды DTO:

  • UserRegisterCommandDto используется для представления данных регистрации пользователя :
public class UserRegisterCommandDto {
private String username;
private String email;
private String password;
}
  • UserUpdatePasswordCommandDto используется для представления данных для обновления текущего пароля пользователя:
public class UserUpdatePasswordCommandDto {
private String oldPassword;
private String password;
}
  • UserTriggerResetPasswordCommandDto используется для представления электронной почты пользователя для запуска сброса пароля путем отправки электронного письма с токеном сброса пароля:
public class UserTriggerResetPasswordCommandDto {
private String email;
}
  • UserChangePasswordCommandDto используется для представления нового пароля пользователя — эта команда вызывается после того, как пользователь использует токен сброса пароля.
public class UserChangePasswordCommandDto {
private String password;
}
  • UserUpdateCommandDto используется для представления данных нового пользователя после изменений:
public class UserUpdateCommandDto {
private Long id;

private boolean enabled;

private Set<Role> roles;
}

4. Вывод

В этом руководстве мы заложили основу для чистой реализации CQRS для Spring REST API.

Следующим шагом будет дальнейшее улучшение API путем определения некоторых отдельных обязанностей (и ресурсов) в их собственных службах, чтобы мы более точно соответствовали архитектуре, ориентированной на ресурсы.