Pas CQRS toe op een Spring REST API

REST Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Overzicht

In dit korte artikel gaan we iets nieuws doen. We gaan een bestaande REST Spring API ontwikkelen en ervoor zorgen dat deze Command Query Responsibility Segregation - CQRS gebruikt.

Het doel is om scheid duidelijk zowel de service- als de controllerlagen om te gaan met Reads - Queries en Writes - Commando's die afzonderlijk in het systeem komen.

Houd er rekening mee dat dit slechts een vroege eerste stap is in de richting van dit soort architectuur, niet "een aankomstpunt". Dat gezegd hebbende - ik ben enthousiast over deze.

Eindelijk: de voorbeeld-API die we gaan gebruiken, is publiceren Gebruiker bronnen en maakt deel uit van onze lopende Reddit-app-casestudy om te laten zien hoe dit werkt, maar natuurlijk zal elke API het doen.

2. De servicelaag

We beginnen eenvoudig - door alleen de lees- en schrijfbewerkingen in onze vorige gebruikersservice te identificeren - en we zullen dat opsplitsen in 2 afzonderlijke services - UserQueryService en UserCommandService:

openbare interface IUserQueryService {Lijst getUsersList (int pagina, int grootte, String sortDir, String sort); String checkPasswordResetToken (lange userId, String-token); String checkConfirmRegistrationToken (String-token); lange countAllUsers (); }
openbare interface IUserCommandService {void registerNewUser (String gebruikersnaam, String e-mail, String wachtwoord, String appUrl); ongeldig updateUserPassword (Gebruiker gebruiker, String wachtwoord, String oldPassword); void changeUserPassword (User user, String-wachtwoord); void resetPassword (String e-mail, String appUrl); void createVerificationTokenForUser (gebruiker gebruiker, tekenreeks token); ongeldig updateUser (gebruiker gebruiker); }

Door deze API te lezen, kunt u duidelijk zien hoe de query-service al het lezen en de commandoservice leest geen gegevens - alle leegte retourneert.

3. De controllerlaag

De volgende - de controllerlaag.

3.1. De querycontroller

Hier is onze UserQueryRestController:

@Controller @RequestMapping (value = "/ api / gebruikers") openbare klasse UserQueryRestController {@Autowired privé IUserQueryService userService; @Autowired privé IScheduledPostQueryService geplandPostService; @Autowired privé ModelMapper modelMapper; @PreAuthorize ("hasRole ('USER_READ_PRIVILEGE')") @RequestMapping (method = RequestMethod.GET) @ResponseBody openbare lijst getUsersList (...) {PagingInfo pagingInfo = nieuwe PagingInfo (pagina, grootte, userService.countAllUsers ()); response.addHeader ("PAGING_INFO", pagingInfo.toString ()); Lijst gebruikers = userService.getUsersList (pagina, grootte, sortDir, sort); return users.stream (). map (gebruiker -> convertUserEntityToDto (gebruiker)). collect (Collectors.toList ()); } private UserQueryDto convertUserEntityToDto (gebruiker gebruiker) {UserQueryDto dto = modelMapper.map (gebruiker, UserQueryDto.class); dto.setScheduledPostsCount (geplandPostService.countScheduledPostsByUser (gebruiker)); terugkeer dto; }}

Wat hier interessant is, is dat de querycontroller alleen query-services injecteert.

Wat nog interessanter zou zijn, is om snijd de toegang van deze controller tot de commandodiensten af - door deze in een aparte module te plaatsen.

3.2. De Command Controller

Nu, hier is onze implementatie van de opdrachtcontroller:

@Controller @RequestMapping (value = "/ api / gebruikers") openbare klasse UserCommandRestController {@Autowired privé IUserCommandService userService; @Autowired privé ModelMapper modelMapper; @RequestMapping (value = "/ registration", method = RequestMethod.POST) @ResponseStatus (HttpStatus.OK) openbaar leeg register (HttpServletRequest-verzoek, @RequestBody UserRegisterCommandDto userDto) {String appUrl = request.getRequestURL (). (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) (getserCommandDto userDto) (getserCommandDto userDto) , userDto.getPassword (), userDto.getOldPassword ()); } @RequestMapping (value = "/ passwordReset", method = RequestMethod.POST) @ResponseStatus (HttpStatus.OK) public void createAResetPassword (HttpServletRequest-verzoek, @RequestBody UserTriggerResetPasswordCommandDto userDto) {tr.verzoek String appUrlest. vervang (request.getRequestURI (), ""); userService.resetPassword (userDto.getEmail (), appUrl); } @RequestMapping (waarde = "/ wachtwoord", method = RequestMethod.POST) @ResponseStatus (HttpStatus.OK) openbare ongeldige changeUserPassword (@RequestBody UserchangePasswordCommandDto userDto) {userService.changeUserPassword (getCurrentUser (), userDto ).get; } @PreAuthorize ("hasRole ('USER_WRITE_PRIVILEGE')") @RequestMapping (value = "/ {id}", method = RequestMethod.PUT) @ResponseStatus (HttpStatus.OK) openbare ongeldige updateUser (@RequestBody UserUpdateCommandDto userServiceDto gebruiker. updateUser (convertToEntity (userDto)); } privégebruiker convertToEntity (UserUpdateCommandDto userDto) {return modelMapper.map (userDto, User.class); }}

Hier gebeuren een paar interessante dingen. Merk eerst op hoe elk van deze API-implementaties een ander commando gebruikt. Dit is voornamelijk om ons een goede basis te geven voor het verder verbeteren van het ontwerp van de API en het extraheren van verschillende bronnen zodra deze zich voordoen.

Een andere reden is dat wanneer we de volgende stap zetten, richting Event Sourcing, we een schone reeks commando's hebben waarmee we werken.

3.3. Afzonderlijke vertegenwoordigingen van bronnen

Laten we nu snel de verschillende weergaven van onze gebruikersbron bespreken, na deze scheiding in opdrachten en vragen:

openbare klasse UserQueryDto {privé Lange id; private String gebruikersnaam; private boolean ingeschakeld; privé Set rollen; privé lang geplandPostsCount; }

Dit zijn onze Command DTO's:

  • UserRegisterCommandDto gebruikt om gebruikersregistratiegegevens weer te geven:
openbare klasse UserRegisterCommandDto {private String gebruikersnaam; privé String-e-mail; privé String-wachtwoord; }
  • UserUpdatePasswordCommandDto gebruikt om gegevens weer te geven om het huidige gebruikerswachtwoord bij te werken:
openbare klasse UserUpdatePasswordCommandDto {privé String oldPassword; privé String-wachtwoord; }
  • UserTriggerResetPasswordCommandDto gebruikt om het e-mailadres van de gebruiker weer te geven om het opnieuw instellen van het wachtwoord te activeren door een e-mail te sturen met het wachtwoord voor het opnieuw instellen van het wachtwoord:
openbare klasse UserTriggerResetPasswordCommandDto {privé String-e-mail; }
  • UserChangePasswordCommandDto gebruikt om het nieuwe gebruikerswachtwoord weer te geven - deze opdracht wordt aangeroepen nadat de gebruiker het wachtwoordhersteltoken heeft gebruikt.
openbare klasse UserChangePasswordCommandDto {privé String-wachtwoord; }
  • UserUpdateCommandDto gebruikt om de gegevens van nieuwe gebruikers weer te geven na wijzigingen:
openbare klasse UserUpdateCommandDto {privé Lange id; private boolean ingeschakeld; privé Set rollen; }

4. Conclusie

In deze tutorial hebben we de basis gelegd voor een schone CQRS-implementatie voor een Spring REST API.

De volgende stap zal zijn om de API te blijven verbeteren door enkele afzonderlijke verantwoordelijkheden (en bronnen) te identificeren in hun eigen services, zodat we beter aansluiten bij een resource-centrische architectuur.

REST onder

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS