Appliquer CQRS à une API REST Spring

Appliquer CQRS à une API Spring REST

1. Vue d'ensemble

Dans cet article rapide, nous allons faire quelque chose de nouveau. Nous allons faire évoluer une API REST Spring existante et la faire utiliser la séparation des responsabilités des requêtes de commande -CQRS.

Le but est queclearly separate both the service and the controller layers traite les lectures - requêtes et écritures - commandes entrant dans le système séparément.

Gardez à l’esprit que ce n’est qu’un début et une première étape vers ce type d’architecture, et non «un point d’arrivée». Cela étant dit, je suis enthousiasmé par celui-ci.

Enfin - l'exemple d'API que nous allons utiliser est la publication des ressources deUser et fait partie de nosReddit app case study en cours pour illustrer comment cela fonctionne - mais bien sûr, n'importe quelle API fera l'affaire.

2. La couche de service

Nous allons commencer simplement - en identifiant simplement les opérations de lecture et d'écriture dans notre précédent service utilisateur - et nous allons diviser cela en 2 services distincts -UserQueryService etUserCommandService:

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

}

En lisant cette API, vous pouvez clairement voir comment le service de requête effectue toutes les lectures etthe command service isn’t reading any data – all void returns.

3. La couche contrôleur

Ensuite, la couche contrôleur.

3.1. Le contrôleur de requête

Voici nosUserQueryRestController:

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

Ce qui est intéressant ici, c'est que le contrôleur de requête n'injecte que des services de requête.

Ce qui serait encore plus intéressant, c'est decut off the access of this controller to the command services - en les plaçant dans un module séparé.

3.2. Le contrôleur de commande

Maintenant, voici notre implémentation de contrôleur de commande:

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

Quelques choses intéressantes se passent ici. Tout d'abord, remarquez comment chacune de ces implémentations d'API utilise une commande différente. Ceci est principalement pour nous donner une bonne base pour améliorer encore la conception de l'API et extraire les différentes ressources au fur et à mesure qu'elles apparaissent.

Une autre raison est que lorsque nous passons à l’étape suivante, vers l’approvisionnement en événements, nous disposons d’un ensemble de commandes propres avec lesquelles nous travaillons.

3.3. Représentations de ressources séparées

Passons maintenant rapidement en revue les différentes représentations de notre ressource Utilisateur, après cette séparation en commandes et requêtes:

public class UserQueryDto {
    private Long id;

    private String username;

    private boolean enabled;

    private Set roles;

    private long scheduledPostsCount;
}

Voici nos DTO de commande:

  • UserRegisterCommandDto utilisé pour représenter les données d'enregistrement de l'utilisateur:

public class UserRegisterCommandDto {
    private String username;
    private String email;
    private String password;
}
  • UserUpdatePasswordCommandDto utilisé pour représenter les données pour mettre à jour le mot de passe utilisateur actuel:

public class UserUpdatePasswordCommandDto {
    private String oldPassword;
    private String password;
}
  • UserTriggerResetPasswordCommandDto utilisé pour représenter l'e-mail de l'utilisateur pour déclencher la réinitialisation du mot de passe en envoyant un e-mail avec un jeton de réinitialisation du mot de passe:

public class UserTriggerResetPasswordCommandDto {
    private String email;
}
  • UserChangePasswordCommandDto utilisé pour représenter le nouveau mot de passe utilisateur - cette commande est appelée après que l'utilisateur utilise le jeton de réinitialisation du mot de passe.

public class UserChangePasswordCommandDto {
    private String password;
}
  • UserUpdateCommandDto utilisé pour représenter les données du nouvel utilisateur après modifications:

public class UserUpdateCommandDto {
    private Long id;

    private boolean enabled;

    private Set roles;
}

4. Conclusion

Dans ce tutoriel, nous avons jeté les bases d'une implémentation propre de CQRS pour une API Spring REST.

La prochaine étape consistera à améliorer sans cesse l'API en identifiant des responsabilités distinctes (et des ressources) dans leurs propres services, de manière à nous aligner plus étroitement sur une architecture centrée sur les ressources.