Appliquer CQRS à une API Spring REST

Haut REST

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

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 de requête de commande - CQRS.

L'objectif est de séparer clairement les couches de service et de contrôleur pour traiter les lectures - les requêtes et les écritures - les commandes entrant dans le système séparément.

Gardez à l'esprit qu'il ne s'agit que d'un premier pas vers ce type d'architecture, et non d'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 de ressources utilisateur et fait partie de notre étude de cas sur l'application Reddit en cours pour illustrer comment cela fonctionne - mais bien sûr, n'importe quelle API fera l'affaire.

2. La couche de service

Nous commencerons simplement - en identifiant simplement les opérations de lecture et d'écriture dans notre précédent service utilisateur - et nous le diviserons en 2 services distincts - UserQueryService et 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); }

En lisant cette API, vous pouvez clairement voir comment le service de requête effectue toute la lecture et le service de commande ne lit aucune donnée - tous les retours sont vides .

3. La couche contrôleur

Ensuite, la couche de contrôleur.

3.1. Le contrôleur de requête

Voici notre 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; } }

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 de couper l'accès de ce contrôleur aux services de commande - en les plaçant dans un module séparé.

3.2. Le contrôleur de commande

Maintenant, voici notre implémentation du 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 produisent ici. Tout d'abord, remarquez comment chacune de ces implémentations d'API utilise une commande différente. C'est principalement pour nous donner une bonne base pour améliorer encore la conception de l'API et extraire différentes ressources au fur et à mesure qu'elles émergent.

Une autre raison est que lorsque nous passons à l'étape suivante, vers Event Sourcing - nous avons un ensemble propre de commandes 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 de l'utilisateur actuel:
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
  • UserTriggerResetPasswordCommandDto utilisé pour représenter l'email 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 didacticiel, nous avons jeté les bases d'une implémentation CQRS propre pour une API Spring REST.

La prochaine étape sera de continuer à améliorer l'API en identifiant certaines responsabilités (et ressources) distinctes dans leurs propres services afin que nous nous alignions plus étroitement sur une architecture centrée sur les ressources.

REST bas

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS