Terapkan CQRS ke Spring REST API

REST Top

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya

1. Ikhtisar

Dalam artikel singkat ini, kami akan melakukan sesuatu yang baru. Kami akan mengembangkan REST Spring API yang sudah ada dan membuatnya menggunakan Segregasi Tanggung Jawab Permintaan Perintah - CQRS.

Tujuannya adalah untuk dengan jelas memisahkan lapisan layanan dan pengontrol untuk menangani Baca - Kueri dan Tulis - Perintah yang masuk ke sistem secara terpisah.

Perlu diingat bahwa ini hanyalah langkah awal pertama menuju arsitektur semacam ini, bukan "titik kedatangan". Yang sedang berkata - Saya senang tentang yang satu ini.

Terakhir - contoh API yang akan kami gunakan adalah mempublikasikan sumber daya Pengguna dan merupakan bagian dari studi kasus aplikasi Reddit kami yang sedang berlangsung untuk memberikan contoh cara kerjanya - tetapi tentu saja, API apa pun bisa digunakan.

2. Lapisan Layanan

Kami akan mulai dengan sederhana - hanya dengan mengidentifikasi operasi baca dan tulis di layanan Pengguna kami sebelumnya - dan kami akan membaginya menjadi 2 layanan terpisah - UserQueryService dan 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); }

Dari membaca API ini, Anda dapat melihat dengan jelas bagaimana layanan kueri melakukan semua pembacaan dan layanan perintah tidak membaca data apa pun - semua void kembali .

3. Lapisan Pengontrol

Selanjutnya - lapisan pengontrol.

3.1. Pengontrol Kueri

Berikut adalah UserQueryRestController kami :

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

Yang menarik di sini adalah bahwa pengontrol kueri hanya memasukkan layanan kueri.

Apa yang lebih menarik adalah memotong akses pengontrol ini ke layanan perintah - dengan menempatkan ini dalam modul terpisah.

3.2. Kontroler Perintah

Sekarang, inilah implementasi pengontrol perintah kami:

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

Beberapa hal menarik sedang terjadi di sini. Pertama - perhatikan bagaimana masing-masing implementasi API ini menggunakan perintah yang berbeda. Ini terutama untuk memberi kami dasar yang baik untuk lebih menyempurnakan desain API dan mengekstrak sumber daya yang berbeda saat mereka muncul.

Alasan lainnya adalah ketika kita mengambil langkah selanjutnya, menuju Event Sourcing - kita memiliki sekumpulan perintah yang bersih yang sedang kita kerjakan.

3.3. Representasi Sumber Daya Terpisah

Sekarang mari kita segera membahas berbagai representasi sumber daya Pengguna kita, setelah pemisahan ini menjadi perintah dan kueri:

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

Berikut adalah Command DTO kami:

  • UserRegisterCommandDto digunakan untuk mewakili data pendaftaran pengguna :
public class UserRegisterCommandDto { private String username; private String email; private String password; }
  • UserUpdatePasswordCommandDto digunakan untuk mewakili data untuk memperbarui kata sandi pengguna saat ini:
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
  • UserTriggerResetPasswordCommandDto digunakan untuk mewakili email pengguna untuk memicu setel ulang sandi dengan mengirimkan email dengan token setel ulang sandi:
public class UserTriggerResetPasswordCommandDto { private String email; }
  • UserChangePasswordCommandDto digunakan untuk mewakili kata sandi pengguna baru - perintah ini dipanggil setelah pengguna menggunakan token reset kata sandi.
public class UserChangePasswordCommandDto { private String password; }
  • UserUpdateCommandDto digunakan untuk mewakili data pengguna baru setelah modifikasi:
public class UserUpdateCommandDto { private Long id; private boolean enabled; private Set roles; }

4. Kesimpulan

Dalam tutorial ini, kami meletakkan dasar menuju implementasi CQRS yang bersih untuk Spring REST API.

Langkah selanjutnya adalah terus meningkatkan API dengan mengidentifikasi beberapa tanggung jawab terpisah (dan Sumber Daya) ke dalam layanan mereka sendiri sehingga kami lebih selaras dengan arsitektur yang berpusat pada Sumber Daya.

REST bawah

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya