1. 概要

この簡単な記事では、何か新しいことをします。 既存のRESTSpringAPIを進化させ、コマンドクエリ責任分離– CQRSを使用するようにします。

目標は、サービスレイヤーとコントローラーレイヤーの両方を明確に分離して、システムに着信する読み取り–クエリと書き込み–コマンドを個別に処理することです。

これは、「到着ポイント」ではなく、この種のアーキテクチャに向けた初期の最初のステップにすぎないことに注意してください。 そうは言っても、私はこれに興奮しています。

最後に、使用するAPIの例は User リソースの公開であり、これがどのように機能するかを例示するために進行中の Redditアプリのケーススタディの一部ですが、もちろん、任意のAPIしましょう。

2. サービスレイヤー

以前のユーザーサービスで読み取り操作と書き込み操作を識別するだけで簡単に始め、それを2つの別々のサービス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);

}

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

もう1つの理由は、イベントソーシングに向けて次のステップに進むときに、使用しているコマンドのクリーンなセットがあることです。

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

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

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