Spring REST APIにCQRSを適用する

1概要

この簡単な記事では、新しいことをします。私たちは既存のREST Spring APIを進化させ、それにコマンドクエリ責任分離を使わせるつもりです - CQRS

目標は、システムに入ってくるReads - QueriesとWrites - Commandsを別々に扱うために、 サービス層とコントローラー層の両方を 明確に分離することです。

これは、このようなアーキテクチャへの最初の一歩に過ぎず、「到着点」ではないことに注意してください。それは言われています - 私はこれに興奮しています。

最後に - これから使用するAPIの例は User リソースの公開であり、現在進行中のリンクの一部です。/case-study-a-reddit-app-with-spring[Reddit appケーススタディ]これがどのように機能するかを例示するために - しかしもちろん、どんな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を読むことで、クエリサービスがどのようにすべての読み取りを行っているのか、またコマンドサービスがどのようなデータも読み取っていないのかが明確にわかります。

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の設計をさらに改善し、出現したときにさまざまなリソースを抽出するための優れた基盤を提供するためです。

もう1つの理由は、イベントソーシングに向けて次のステップを踏み出すときに、作業中のコマンドのセットがきれいになっていることです。

3.3. 個別のリソース表現

ここで、コマンドとクエリに分離した後、ユーザーリソースのさまざまな表現について簡単に説明しましょう。

public class UserQueryDto {
    private Long id;

    private String username;

    private boolean enabled;

    private Set<Role> roles;

    private long scheduledPostsCount;
}

これが私たちのCommand 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 は、ユーザーのEメールアドレスを表します。

パスワード再設定トークンを含むEメールを送信してパスワード再設定をトリガーします。

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結論

このチュートリアルでは、Spring REST API用のクリーンなCQRS実装に向けた基礎を築きました。

次のステップは、リソース中心のアーキテクチャーとより緊密に連携するように、いくつかの個別の責任(およびリソース)を独自のサービスに分類して、APIを改善し続けることです。

前の投稿:JMockitの期待へのガイド
次の投稿:口ひげ入門