در این بخش خواهیم دید که چگونه یک REST api آماده را با CQRS تکامل بدهیم و از ویژگی های آن استفاده کنیم و لایه Service را از Controller جدا کنیم تا بتوانیم Query ها و Command ها را جداگانه مدیریت کنیم
توجه داشته باشید که مطالب این بخش برای کسانی که روی معماری و فرآیند ها در CQRS آشنایی دارند مفید خواهد بود لذا اگر پیش زمینه ندارید بهتر است قبل از خواندن ادامه مطلب نگاهی به آن داشته باشید
لایه Service :
در این لایه عملیات خواندنی و نوشتنی کلاینت ها را مشخص میکنیم که شامل یک کلاس برای Query ها و یک کلاس برای Command ها میشود
public interface IUserQueryService { List<User> 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); }
* همانطور که در معماری CQRS و کد بالا قابل مشاهده است بخش Command ها مقدار برگشتی ندارند و دیتایی خوانده نمیشود
لایه کنترلر Query سرویس :
@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<UserQueryDto> getUsersList(...) { PagingInfo pagingInfo = new PagingInfo(page, size, userService.countAllUsers()); response.addHeader("PAGING_INFO", pagingInfo.toString()); List<User> 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; } }
نکته جالبی که وجود دارد در لایه Controller سرویس query به عنوان یک Bean تزریق شده است و وظیفه رسیدگی به درخواست های کاربر را دارد و از لایه سرویس Command ها ایزوله است و در یک ماژول جداگانه قابل استفاده است
لایه کنترلر Command سرویس :
@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); } }
ویژگی جالبی که استفاده از معماری CQRS به API ما میدهد در هر یک از پیاده سازی ها دستورات متفاوتی استفاده شده و در آینده اگر بخواهیم تغییری با بهبودی بدهیم راحت تر قابل انجام است و در ثانی اگر بخواهیم Event Sourcing داشته باشیم مجموعه فرامین مرتبی داریم که میتوانیم از انها استفاده کنیم
ارائه منابع جداگانه :
در بخش قبل دیدیم که چطور فرامین را توانستیم از هم جدا کنیم حالا نوبت به جدا سازی Resource هایی است که در بخش Command ها و Query ها نیاز به استفاده داریم
کلاس DTO نگهداری اطلاعات کاربر بعد از اجرای Query :
public class UserQueryDto { private Long id; private String username; private boolean enabled; private Set<Role> roles; private long scheduledPostsCount; }
کلاس DTO مورد نیاز برای Command ثبت نام کاربر جدید :
public class UserRegisterCommandDto { private String username; private String email; private String password; }
کلاس DTO مورد نیاز برای Command آپدیت کردن password کاربر :
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
کلاس DTO مورد نیاز Command ریست password و بصورت token موقتا نگهداری میشود :
public class UserTriggerResetPasswordCommandDto { private String email; }
کلاس DTO برای Command ریست password و نگهداری پسورد جدید ریست شده :
public class UserChangePasswordCommandDto { private String password; }
کلاس DTO نگهداری اطلاعات کاربر مربوط به Command تغییر دادن اطلاعات کاربر :
public class UserUpdateCommandDto { private Long id; private boolean enabled; private Set<Role> roles; }