JavaRush /Курсы /Модуль 5. Spring /Лекция 216: Введение в CQRS (Command Query Responsibility...

Лекция 216: Введение в CQRS (Command Query Responsibility Segregation)

Модуль 5. Spring
22 уровень , 5 лекция
Открыта

CQRS буквально расшифровывается как Command Query Responsibility Segregation, что переводится на русский как "разделение ответственности за команды и запросы". Но давайте не будем бросаться сложными терминами и начнём с сути.

В традиционных приложениях мы часто используем одну и ту же модель данных и для чтения, и для записи. Например, представьте сущность Order в системе онлайн-магазина. Мы используем один и тот же объект, чтобы:

  • Создать новый заказ (запись).
  • Получить информацию о заказе (чтение).

Всё выглядит замечательно... до тех пор, пока объём данных и трафика не вырастает до небес. Тогда появляются проблемы:

  • Разные требования к чтению и записи. Например, при чтении пользователю может понадобиться информация из нескольких связанных таблиц, а при записи — только определённые поля.
  • Сложность масштабирования. Запросы на чтение чаще всего составляют 80–90% нагрузки. Если вы используете одну модель, то и чтение, и запись будут "толкаться локтями" за ресурсы.
  • Эволюция модели. Если вы меняете модель для записи, это может привести к необходимости переработать всю логику чтения, и наоборот.

Основная идея CQRS

CQRS предлагает нам разделить модели данных и пути обработки для чтения (Query) и записи (Command):

  1. Command (команды) — используются для изменения состояния системы: обновления, создания, удаления данных.
  2. Query (запросы) — используются для получения данных, причём они могут быть оптимизированы именно под операции чтения.

Пример (аналогия): представьте кухню ресторана. Официанты принимают заказы (команды), а шеф-повар готовит блюда. Но для того, чтобы показать заказ клиенту, официант берёт готовое блюдо (запрос). Команды и запросы работают в разных плоскостях!


Основные принципы CQRS

Прежде чем углубиться в реальную реализацию, давайте разберём ключевые принципы CQRS.

1. Разделение моделей данных

В CQRS вы используете разные модели для операций чтения и записи. Модель команд может быть простой и содержать только те данные, которые нужны для изменения состояния. Например, при создании заказа может понадобиться лишь идентификатор пользователя и список товаров.

Модель запросов, напротив, может быть сложной, включать агрегацию данных из нескольких источников и быть оптимизированной для быстрого чтения. Иногда она даже хранит данные в другой базе!

2. Механизм обработки команд

Команды в CQRS — это не просто "сохранить объект в базу". Это отдельные объекты, которые:

  • Описывают действие: CreateOrderCommand, CancelOrderCommand.
  • Содержат только ту информацию, которая нужна для выполнения конкретной операции.
  • Реализуются через обработчики команд (command handlers).

3. Механизм обработки запросов

Запросы в CQRS используются исключительно для чтения данных. Они:

  • Могут агрегировать данные из нескольких источников (например, SQL-базы и кэша).
  • В некоторых случаях имеют собственное хранилище, например, денормализованную базу данных.

Причины использования CQRS

Вам может показаться, что CQRS — это лишняя сложность, особенно если у вас небольшой проект. Но в определённых ситуациях этот паттерн просто необходим:

1. Высокая нагрузка на чтение

Если в вашей системе огромное количество запросов на чтение, а данные нужно собирать из нескольких источников, CQRS поможет разделить нагрузку.

2. Разные требования к чтению и записи

Когда чтение и запись имеют разные бизнес-правила. Например, при записи нужно проверить права пользователя и выполнить сложные валидации, тогда как чтение требует только быстрый доступ к данным.

3. Масштабируемость

Вы можете отдельно масштабировать компоненты для обработки запросов на чтение и записи. Например:

  • Для чтения можно добавить больше реплик базы данных.
  • Для записи — настроить мастер-базу.

4. Эволюция модели

Если разделить модели для чтения и записи, вы уменьшите зависимость одного типа операций от другого. Это делает вашу систему гибче.


Пример

Представим, что у нас есть система онлайн-магазина. Клиенты могут создавать заказы через REST API. Нам нужно:

  1. Принимать новые заказы (операция записи).
  2. Возвращать пользователю список его заказов с информацией о доставке (операция чтения).

Реализация 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());
    }
}

Преимущества разделения на команды и запросы

  1. Простота тестирования. Можно тестировать команды и запросы отдельно.
  2. Оптимизация. Каждая часть оптимизируется под свои задачи (написание данных или их чтение).
  3. Подготовка к Event Sourcing. CQRS отлично "собирается" с Event Sourcing, о чём мы поговорим в следующих лекциях.

Чем закончится ваша CQRS-авантюра?

CQRS — мощный паттерн, который отлично дополняет микросервисные системы. Однако применять его нужно с умом! Не каждая задача требует раздельных моделей данных. Помните золотое правило: "Не добавляйте сложности, пока это не нужно". В следующих лекциях мы углубимся в связь CQRS с Event Sourcing и узнаем, как это может выстрелить в самых неожиданных местах.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ