Сьогодні будемо проєктувати реальну систему, застосовуючи підхід CQRS. Пройдемо всі етапи — від постановки задачі до реалізації ключових компонентів. Уявіть, що ми робимо спрощену систему керування замовленнями, щоб клієнти могли зробити замовлення, а менеджери — переглядати список замовлень. Пора зануритись у процес!
Постановка задачі
Ми розробляємо мікросервіс для керування замовленнями в інтернет-магазині. Вимоги такі:
- Користувачі можуть створювати замовлення.
- Адміністратори та менеджери можуть переглядати список замовлень, фільтрувати замовлення за статусом та отримувати інші звіти.
- Робота з даними має бути максимально ефективною.
Вимоги
- Має бути чіткий розподіл операцій читання і запису.
- Команди (зміни даних) і запити (читання даних) не повинні заважати одне одному. Наприклад, тривалі операції читання звітів не повинні блокувати створення нових замовлень.
- Система має бути готова до масштабування, з можливістю обробляти великий обсяг користувачів.
Проєктування
Архітектура з CQRS
Спочатку розділимо відповідальність. Виділяємо дві моделі:
- Командна модель (Write Model): відповідає за зміну стану системи (наприклад, створення та оновлення замовлень).
- Запитна модель (Read Model): дозволяє отримувати дані в оптимізованому вигляді для читання (наприклад, список замовлень, деталі по замовленню).
Ось спрощена схема архітектури:
[ Клієнт ]
|
[ API Gateway ]
|
[ Command Service ] <---> [ Write Database ]
[ Query Service ] <---> [ Read Database ]
Вибір технологій
- Command Side (Write): будемо використовувати Spring Boot з JPA/Hibernate для роботи з базою даних, де будуть зберігатися замовлення.
- Query Side (Read): оптимізуємо читання даних, використовуючи структуру, придатну для читання, з можливим використанням окремих таблиць або проекцій (наприклад, Elasticsearch для Full-Text Search).
Моделі даних
1. Командна модель:
Створення замовлення включає:
- Ідентифікатор замовлення
- Список товарів
- Дату створення
- Статус замовлення
Приклад моделі:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String customerName;
@Column(nullable = false)
private String status;
@OneToMany
private List<OrderItem> items;
// Getters and Setters
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private int quantity;
private double price;
// Getters and Setters
}
2. Запитна модель:
Для запитів дані оптимізуються заздалегідь. Наприклад, робимо плоску таблицю, яка одразу містить всю інформацію про замовлення, щоб не робити складних join-ів.
Приклад моделі:
public class OrderReadModel {
private Long id;
private String customerName;
private String status;
private List<OrderItemReadModel> items;
// Getters and Setters
}
public class OrderItemReadModel {
private String productName;
private int quantity;
private double price;
// Getters and Setters
}
Реалізація
Почнемо зі сторони запису. Використаємо контролер для обробки команд.
@RestController
@RequestMapping("/orders")
public class OrderCommandController {
private final OrderService orderService;
public OrderCommandController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody OrderRequest request) {
orderService.createOrder(request);
return ResponseEntity.ok("Order created successfully!");
}
}
Сервісний шар:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void createOrder(OrderRequest request) {
Order order = new Order();
order.setCustomerName(request.getCustomerName());
order.setStatus("NEW");
List<OrderItem> items = request.getItems().stream()
.map(item -> {
OrderItem orderItem = new OrderItem();
orderItem.setProductName(item.getProductName());
orderItem.setQuantity(item.getQuantity());
orderItem.setPrice(item.getPrice());
return orderItem;
}).collect(Collectors.toList());
order.setItems(items);
orderRepository.save(order);
}
}
DTO (Data Transfer Object) для запиту:
public class OrderRequest {
private String customerName;
private List<OrderItemRequest> items;
// Getters and Setters
}
Запити обробляються окремо. Створимо Query Service:
@RestController
@RequestMapping("/orders/query")
public class OrderQueryController {
private final OrderQueryService orderQueryService;
public OrderQueryController(OrderQueryService orderQueryService) {
this.orderQueryService = orderQueryService;
}
@GetMapping("/{id}")
public ResponseEntity<OrderReadModel> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderQueryService.getOrder(id));
}
@GetMapping
public ResponseEntity<List<OrderReadModel>> getAllOrders() {
return ResponseEntity.ok(orderQueryService.getAllOrders());
}
}
Сервісний шар:
@Service
public class OrderQueryService {
private final OrderReadRepository orderReadRepository;
public OrderQueryService(OrderReadRepository orderReadRepository) {
this.orderReadRepository = orderReadRepository;
}
public OrderReadModel getOrder(Long id) {
// Використовуємо репозиторій для отримання даних з Read Database
return orderReadRepository.findOrderById(id);
}
public List<OrderReadModel> getAllOrders() {
return orderReadRepository.findAll();
}
}
Репозиторій для читання:
public interface OrderReadRepository {
OrderReadModel findOrderById(Long id);
List<OrderReadModel> findAll();
}
Примітки та покращення
- Синхронізація даних між Write і Read моделями:
- При оновленні даних у Write Database можна використовувати події (наприклад, Kafka), щоб оновлювати Read Database.
- Вибір Read Database:
- У простих сценаріях можна використовувати ту ж базу даних (але з окремими таблицями). У складних випадках для запитів можна застосувати NoSQL бази даних, такі як Elasticsearch.
- Кешування:
- Для прискорення запитів можна використовувати кеш, наприклад Redis.
- Обробка помилок:
- Обов'язково обробіть помилки: наприклад, валідацію даних, недоступність бази даних тощо.
Отже, завдяки CQRS ми розділили операції зміни та читання даних, що покращує продуктивність і масштабованість системи. У реальних проєктах підхід CQRS дозволяє впоратись з високими навантаженнями і складністю обробки даних, даючи більш керовану архітектуру.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ