CQRSをSpring REST APIに適用する
1. 概要
この簡単な記事では、何か新しいことをします。 既存のRESTSpring APIを進化させ、Command Query Responsibility Segregation –CQRSを使用するようにします。
目標は、clearly separate both the service and the controller layersを使用して、システムに個別に入力される読み取り–クエリと書き込み–コマンドを処理することです。
これは、この種のアーキテクチャに向けた初期の最初のステップにすぎず、「到着点」ではないことに注意してください。 そうは言っても、私はこれに興奮しています。
最後に、使用する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を読み取ることで、クエリサービスがすべての読み取りとthe command service isn’t reading any data – all void returnsをどのように実行しているかを明確に確認できます。
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 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;
}
}
ここで興味深いのは、クエリコントローラーがクエリサービスのみを挿入していることです。
さらに興味深いのは、cut off the access of this controller to the command servicesを別のモジュールに配置することです。
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 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 roles;
}
4. 結論
このチュートリアルでは、Spring REST APIのクリーンなCQRS実装に向けて基礎を築きました。
次のステップは、リソース中心のアーキテクチャとより密接に連携するために、独自のサービスにいくつかの別個の責任(およびリソース)を特定することにより、APIの改善を続けることです。