Aplicar CQRS a uma API REST do Spring
1. Visão geral
Neste artigo rápido, vamos fazer algo novo. Vamos desenvolver uma API REST Spring existente e torná-la usando Segregação de Responsabilidade de Consulta de Comando -CQRS.
O objetivo éclearly separate both the service and the controller layers lidar com leituras - consultas e gravações - comandos que entram no sistema separadamente.
Lembre-se de que este é apenas um primeiro passo inicial para esse tipo de arquitetura, não "um ponto de chegada". Dito isso - estou animado com este.
Finalmente - a API de exemplo que vamos usar está publicandoUser recursos e faz parte de nossoReddit app case study em andamento para exemplificar como isso funciona - mas é claro, qualquer API serve.
2. A Camada de Serviço
Começaremos de maneira simples - identificando apenas as operações de leitura e gravação em nosso serviço de usuário anterior - e dividiremos isso em 2 serviços separados -UserQueryServiceeUserCommandService:
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);
}
Ao ler esta API, você pode ver claramente como o serviço de consulta está fazendo todas as leituras ethe command service isn’t reading any data – all void returns.
3. A camada do controlador
Em seguida - a camada do controlador.
3.1. O Query Controller
Aqui está nossoUserQueryRestController:
@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;
}
}
O que é interessante aqui é que o controlador de consulta está apenas injetando serviços de consulta.
O que seria ainda mais interessante écut off the access of this controller to the command services - colocando-os em um módulo separado.
3.2. O controlador de comando
Agora, aqui está nossa implementação do controlador de 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);
}
}
Algumas coisas interessantes estão acontecendo aqui. Primeiro, observe como cada uma dessas implementações de API está usando um comando diferente. Isso é principalmente para nos dar uma boa base para melhorar ainda mais o design da API e extrair diferentes recursos à medida que eles surgirem.
Outro motivo é que, quando damos o próximo passo, em direção ao Event Sourcing - temos um conjunto limpo de comandos com os quais estamos trabalhando.
3.3. Representações de recursos separados
Vamos agora examinar rapidamente as diferentes representações de nosso recurso de usuário, após essa separação em comandos e consultas:
public class UserQueryDto {
private Long id;
private String username;
private boolean enabled;
private Set roles;
private long scheduledPostsCount;
}
Aqui estão nossos DTOs de comando:
-
UserRegisterCommandDto usado para representar os dados de registro do usuário:
public class UserRegisterCommandDto {
private String username;
private String email;
private String password;
}
-
UserUpdatePasswordCommandDto usado para representar os dados para atualizar a senha do usuário atual:
public class UserUpdatePasswordCommandDto {
private String oldPassword;
private String password;
}
-
UserTriggerResetPasswordCommandDto usado para representar o e-mail do usuário para acionar a redefinição de senha enviando um e-mail com token de redefinição de senha:
public class UserTriggerResetPasswordCommandDto {
private String email;
}
-
UserChangePasswordCommandDto usado para representar a nova senha do usuário - este comando é chamado após o usuário usar o token de redefinição de senha.
public class UserChangePasswordCommandDto {
private String password;
}
-
UserUpdateCommandDto usado para representar os dados do novo usuário após as modificações:
public class UserUpdateCommandDto {
private Long id;
private boolean enabled;
private Set roles;
}
4. Conclusão
Neste tutorial, lançamos as bases para uma implementação CQRS limpa para uma API Spring REST.
O próximo passo será continuar melhorando a API, identificando algumas responsabilidades (e Recursos) separadas em seus próprios serviços, para que possamos nos alinhar mais com uma arquitetura centrada em Recursos.