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

Лекція 216: Вступ до CQRS (Command Query Responsibility Segregation)

Модуль 5. Spring
Рівень 14 , Лекція 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 і дізнаємося, як це може вистрілити у найнеочікуваніших місцях.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ