Сегодня мы будем проектировать реальную систему, применяя подход 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. Запросная модель:
Для запросов данные оптимизируются заранее. Например, мы создаём плоскую таблицу, которая сразу содержит всю информацию о заказе, чтобы не делать сложных джойнов.
Пример модели:
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 позволяет справляться с высокими нагрузками и сложностями обработки данных, предоставляя более управляемую архитектуру.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ