CQRS буквально расшифровывается как Command Query Responsibility Segregation, что переводится на русский как "разделение ответственности за команды и запросы". Но давайте не будем бросаться сложными терминами и начнём с сути.
В традиционных приложениях мы часто используем одну и ту же модель данных и для чтения, и для записи. Например, представьте сущность Order в системе онлайн-магазина. Мы используем один и тот же объект, чтобы:
- Создать новый заказ (запись).
- Получить информацию о заказе (чтение).
Всё выглядит замечательно... до тех пор, пока объём данных и трафика не вырастает до небес. Тогда появляются проблемы:
- Разные требования к чтению и записи. Например, при чтении пользователю может понадобиться информация из нескольких связанных таблиц, а при записи — только определённые поля.
- Сложность масштабирования. Запросы на чтение чаще всего составляют 80–90% нагрузки. Если вы используете одну модель, то и чтение, и запись будут "толкаться локтями" за ресурсы.
- Эволюция модели. Если вы меняете модель для записи, это может привести к необходимости переработать всю логику чтения, и наоборот.
Основная идея CQRS
CQRS предлагает нам разделить модели данных и пути обработки для чтения (Query) и записи (Command):
- Command (команды) — используются для изменения состояния системы: обновления, создания, удаления данных.
- Query (запросы) — используются для получения данных, причём они могут быть оптимизированы именно под операции чтения.
Пример (аналогия): представьте кухню ресторана. Официанты принимают заказы (команды), а шеф-повар готовит блюда. Но для того, чтобы показать заказ клиенту, официант берёт готовое блюдо (запрос). Команды и запросы работают в разных плоскостях!
Основные принципы CQRS
Прежде чем углубиться в реальную реализацию, давайте разберём ключевые принципы CQRS.
1. Разделение моделей данных
В CQRS вы используете разные модели для операций чтения и записи. Модель команд может быть простой и содержать только те данные, которые нужны для изменения состояния. Например, при создании заказа может понадобиться лишь идентификатор пользователя и список товаров.
Модель запросов, напротив, может быть сложной, включать агрегацию данных из нескольких источников и быть оптимизированной для быстрого чтения. Иногда она даже хранит данные в другой базе!
2. Механизм обработки команд
Команды в CQRS — это не просто "сохранить объект в базу". Это отдельные объекты, которые:
- Описывают действие:
CreateOrderCommand,CancelOrderCommand. - Содержат только ту информацию, которая нужна для выполнения конкретной операции.
- Реализуются через обработчики команд (command handlers).
3. Механизм обработки запросов
Запросы в CQRS используются исключительно для чтения данных. Они:
- Могут агрегировать данные из нескольких источников (например, SQL-базы и кэша).
- В некоторых случаях имеют собственное хранилище, например, денормализованную базу данных.
Причины использования CQRS
Вам может показаться, что CQRS — это лишняя сложность, особенно если у вас небольшой проект. Но в определённых ситуациях этот паттерн просто необходим:
1. Высокая нагрузка на чтение
Если в вашей системе огромное количество запросов на чтение, а данные нужно собирать из нескольких источников, CQRS поможет разделить нагрузку.
2. Разные требования к чтению и записи
Когда чтение и запись имеют разные бизнес-правила. Например, при записи нужно проверить права пользователя и выполнить сложные валидации, тогда как чтение требует только быстрый доступ к данным.
3. Масштабируемость
Вы можете отдельно масштабировать компоненты для обработки запросов на чтение и записи. Например:
- Для чтения можно добавить больше реплик базы данных.
- Для записи — настроить мастер-базу.
4. Эволюция модели
Если разделить модели для чтения и записи, вы уменьшите зависимость одного типа операций от другого. Это делает вашу систему гибче.
Пример
Представим, что у нас есть система онлайн-магазина. Клиенты могут создавать заказы через REST API. Нам нужно:
- Принимать новые заказы (операция записи).
- Возвращать пользователю список его заказов с информацией о доставке (операция чтения).
Реализация CQRS
Модель команд (Command)
На шаге записи мы хотим сохранить только нужные данные. Вот пример класса команды:
// Команда для создания нового заказа
public class CreateOrderCommand {
private final UUID userId;
private final List<OrderItem> items;
// Конструктор
public CreateOrderCommand(UUID userId, List<OrderItem> items) {
this.userId = userId;
this.items = items;
}
// Геттеры
public UUID getUserId() {
return userId;
}
public List<OrderItem> getItems() {
return items;
}
}
// Вспомогательный класс для описания заказанных товаров
public class OrderItem {
private final String productId;
private final int quantity;
public OrderItem(String productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
// Геттеры
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
}
Обработчик команд
Теперь создадим обработчик, который будет обрабатывать команду CreateOrderCommand:
@Component
public class CreateOrderCommandHandler {
private final OrderRepository orderRepository;
// Внедрение зависимостей через конструктор
public CreateOrderCommandHandler(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void handle(CreateOrderCommand command) {
// Преобразуем команду в сущность Order
Order order = new Order(
UUID.randomUUID(), // Генерируем уникальный идентификатор заказа
command.getUserId(),
command.getItems(),
LocalDateTime.now()
);
// Сохраняем заказ в базе данных
orderRepository.save(order);
}
}
Модель запроса (Query)
Теперь создадим модель для чтения данных. Она будет содержать всю необходимую информацию для клиента:
// Модель для чтения данных о заказе
public class OrderQueryModel {
private final UUID orderId;
private final String status;
private final LocalDateTime createdDate;
// Конструктор
public OrderQueryModel(UUID orderId, String status, LocalDateTime createdDate) {
this.orderId = orderId;
this.status = status;
this.createdDate = createdDate;
}
// Геттеры
public UUID getOrderId() {
return orderId;
}
public String getStatus() {
return status;
}
public LocalDateTime getCreatedDate() {
return createdDate;
}
}
Обработчик запросов
Теперь мы реализуем обработчик для получения информации о заказах через оптимизированный запрос:
@Component
public class OrderQueryHandler {
private final OrderViewRepository orderViewRepository;
// Внедрение зависимостей через конструктор
public OrderQueryHandler(OrderViewRepository orderViewRepository) {
this.orderViewRepository = orderViewRepository;
}
public List<OrderQueryModel> handle(UUID userId) {
// Получаем список заказов из денормализованной базы данных
return orderViewRepository.findByUserId(userId)
.stream()
.map(order -> new OrderQueryModel(order.getId(), order.getStatus(), order.getCreatedDate()))
.collect(Collectors.toList());
}
}
Преимущества разделения на команды и запросы
- Простота тестирования. Можно тестировать команды и запросы отдельно.
- Оптимизация. Каждая часть оптимизируется под свои задачи (написание данных или их чтение).
- Подготовка к Event Sourcing. CQRS отлично "собирается" с Event Sourcing, о чём мы поговорим в следующих лекциях.
Чем закончится ваша CQRS-авантюра?
CQRS — мощный паттерн, который отлично дополняет микросервисные системы. Однако применять его нужно с умом! Не каждая задача требует раздельных моделей данных. Помните золотое правило: "Не добавляйте сложности, пока это не нужно". В следующих лекциях мы углубимся в связь CQRS с Event Sourcing и узнаем, как это может выстрелить в самых неожиданных местах.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ