Applicare CQRS a un'API REST di Spring

REST Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

In questo breve articolo, faremo qualcosa di nuovo. Svilupperemo un'API REST Spring esistente e le faremo utilizzare Command Query Responsibility Segregation - CQRS.

L'obiettivo è separare chiaramente sia il livello del servizio che quello del controller per gestire le letture - query e scritture - comandi che entrano nel sistema separatamente.

Tieni presente che questo è solo un primo passo verso questo tipo di architettura, non "un punto di arrivo". Detto questo, sono entusiasta di questo.

Infine, l'API di esempio che utilizzeremo è la pubblicazione delle risorse utente e fa parte del nostro case study sull'app Reddit in corso per esemplificare come funziona, ma ovviamente qualsiasi API andrà bene.

2. Il livello di servizio

Inizieremo in modo semplice, identificando semplicemente le operazioni di lettura e scrittura nel nostro precedente servizio utente, e lo divideremo in 2 servizi separati: UserQueryService e 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); }

Dalla lettura di questa API puoi vedere chiaramente come il servizio di query sta eseguendo tutta la lettura e il servizio di comando non sta leggendo alcun dato: tutto il void ritorna .

3. Il Controller Layer

Avanti: il livello del controller.

3.1. Il Query Controller

Ecco il nostro 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; } }

Ciò che è interessante qui è che il controller di query sta solo iniettando servizi di query.

Ciò che sarebbe ancora più interessante è tagliare l'accesso di questo controller ai servizi di comando, inserendoli in un modulo separato.

3.2. Il controller di comando

Ora, ecco la nostra implementazione del controller di comando:

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

Qui stanno accadendo alcune cose interessanti. Innanzitutto, nota come ciascuna di queste implementazioni API utilizza un comando diverso. Questo è principalmente per fornirci una buona base per migliorare ulteriormente il design dell'API ed estrarre risorse diverse man mano che emergono.

Un altro motivo è che quando facciamo il passo successivo, verso Event Sourcing, abbiamo un set pulito di comandi con cui stiamo lavorando.

3.3. Rappresentazioni separate delle risorse

Passiamo ora rapidamente alle diverse rappresentazioni della nostra risorsa Utente, dopo questa separazione in comandi e query:

public class UserQueryDto { private Long id; private String username; private boolean enabled; private Set roles; private long scheduledPostsCount; }

Ecco i nostri DTO di comando:

  • UserRegisterCommandDto utilizzato per rappresentare i dati di registrazione dell'utente :
public class UserRegisterCommandDto { private String username; private String email; private String password; }
  • UserUpdatePasswordCommandDto utilizzato per rappresentare i dati per aggiornare la password dell'utente corrente:
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
  • UserTriggerResetPasswordCommandDto utilizzato per rappresentare l'e-mail dell'utente per attivare la reimpostazione della password inviando un'e-mail con il token di reimpostazione della password:
public class UserTriggerResetPasswordCommandDto { private String email; }
  • UserChangePasswordCommandDto utilizzato per rappresentare la nuova password utente: questo comando viene chiamato dopo che l'utente utilizza il token di reimpostazione della password.
public class UserChangePasswordCommandDto { private String password; }
  • UserUpdateCommandDto utilizzato per rappresentare i dati del nuovo utente dopo le modifiche:
public class UserUpdateCommandDto { private Long id; private boolean enabled; private Set roles; }

4. Conclusione

In questo tutorial, abbiamo gettato le basi per un'implementazione CQRS pulita per un'API REST di Spring.

Il passo successivo consisterà nel continuare a migliorare l'API identificando alcune responsabilità (e risorse) separate nei propri servizi in modo da allinearci più strettamente con un'architettura incentrata sulle risorse.

REST fondo

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO