Рубрики
Без рубрики

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

Узнайте, как начать перемещение существующего API Spring REST в сторону архитектуры, ориентированной на CQRS.

Автор оригинала: Eugen Paraschiv.

1. Обзор

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

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

Имейте в виду, что это всего лишь ранний первый шаг к такого рода архитектуре, а не “точка прибытия”. Тем не менее, я в восторге от этого.

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

2. Уровень Обслуживания

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

public interface IUserQueryService {

    List 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. Контроллер Запросов

Вот наш Пользовательский запрос RestController :

@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 getUsersList(...) {
        PagingInfo pagingInfo = new PagingInfo(page, size, userService.countAllUsers());
        response.addHeader("PAGING_INFO", pagingInfo.toString());
        
        List 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 и извлечения различных ресурсов по мере их появления.

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

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

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

public class UserQueryDto {
    private Long id;

    private String username;

    private boolean enabled;

    private Set roles;

    private long scheduledPostsCount;
}

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

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

    private boolean enabled;

    private Set roles;
}

4. Заключение

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

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