JavaRush /Курси /Модуль 5. Spring /Лекція 219: Як пов'язані CQRS і Event Sourcing

Лекція 219: Як пов'язані CQRS і Event Sourcing

Модуль 5. Spring
Рівень 14 , Лекція 8
Відкрита

Event Sourcing — це підхід до зберігання даних, де головним джерелом істини (source of truth) є події. Замість того, щоб зберігати поточний стан об'єкта, ми зберігаємо всі зміни, які коли-небудь з ним відбувалися.

Як це працює? Уяви банківський рахунок. Замість того, щоб зберігати, скажімо, поточний баланс, ми зберігаємо всі операції: поповнення, зняття, перекази. Поточний стан балансу — це результат "програвання" усіх цих операцій. Це як дивитися історію браузера: щоб зрозуміти, як ти опинився на поточній сторінці, ти просто переглядаєш всі попередні переходи.


Події:
1. Поповнення: +1000 умовні одиниці
2. Зняття: -200 умовні одиниці
3. Поповнення: +500 умовні одиниці

Поточний стан:
Баланс: 1300 умовні одиниці (обчислюється з подій)

Це дозволяє не тільки знати "де ти зараз", але й зрозуміти "як ти сюди потрапив".


Зв'язок між CQRS і Event Sourcing

Щоб зрозуміти, як ці два підходи пов'язані, давай розберемо їх по поличках.

CQRS: розділення світу на два

CQRS ділить операції системи на дві сфери: командний шар (Command) і шар запитів (Query). Команди змінюють стан системи, а запити лише читають дані. І от тут з'являється цікава ідея: а що як для зміни даних (Command) використовувати події?

Event Sourcing як потужна основа CQRS

Event Sourcing ідеально лягає на Command-частину CQRS. Замість того, щоб миттєво зберігати зміни в базу даних, ви зберігаєте події, які описують зміни. А вже потім, на основі цих подій, оновлюєте стан або створюєте нові моделі для читання даних (Query).

Дивись, як це виглядає на практиці:

  1. Користувач відправляє команду зняти гроші (наприклад, 500 умовних одиниць з рахунку).
  2. Замість того, щоб одразу оновити баланс в базі даних, система створює подію: MoneyWithdrawn {amount: 500}.
  3. Ця подія зберігається в сховищі подій.
  4. Сховище подій пізніше обробляє цю подію, оновлюючи моделі для читання (наприклад, "баланс користувача").

Отже, основний принцип тут: "Спочатку подія, потім наслідки".


Переваги їх інтеграції

Коли ти об'єднуєш CQRS і Event Sourcing, ти отримуєш кілька класних бонусів:

  1. Повна історія змін. Ти можеш повернутися назад у часі і зрозуміти, як об'єкт прийшов до поточного стану. Це дуже зручно для аудиту або відладки.
  2. Можливість повторного відтворення. Якщо потрібно перерахувати поточний стан в іншій базі даних або відновити систему після збою — ти просто програєш усі події.
  3. Автоматичне відділення команд і запитів. Event Sourcing органічно вписується в підхід CQRS. Події — це результат команд, а підготовлені моделі даних для запитів (Query) — це твій читабельний результат.
  4. Асинхронність. Подію можна обробити пізніше. Це знижує навантаження на Command-шар і дозволяє системі масштабуватися.

Реалізація на практиці

Давай спробуємо реалізувати базовий приклад з використанням CQRS разом з Event Sourcing. Ми продовжимо розробку нашого додатку для управління замовленнями в мікросервісній архітектурі.

Припустимо, наше додаток обробляє замовлення. Нам необхідно:

  • Створити замовлення.
  • Оновити статус замовлення.
  • Підготувати модель для користувача, щоб він міг бачити поточний статус усіх своїх замовлень.

Крок 1: Створення подій

Створимо Java-класи, які представляють наші події.


// Подія створення замовлення
public class OrderCreatedEvent {
    private final String orderId;
    private final String customerId;
    private final String product;

    public OrderCreatedEvent(String orderId, String customerId, String product) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.product = product;
    }

    // Гетери
}


// Подія оновлення статусу замовлення
public class OrderStatusUpdatedEvent {
    private final String orderId;
    private final String status;

    public OrderStatusUpdatedEvent(String orderId, String status) {
        this.orderId = orderId;
        this.status = status;
    }

    // Гетери
}

Крок 2: Сховище подій

Нам потрібно місце, де ми будемо зберігати події. Нехай для початку це буде простий список.


import java.util.ArrayList;
import java.util.List;

public class EventStore {
    private final List<Object> events = new ArrayList<>();

    public void saveEvent(Object event) {
        events.add(event);
    }

    public List<Object> getEvents() {
        return new ArrayList<>(events);
    }
}

Крок 3: Command Handler

Обробник команд відповідає за створення подій на основі вхідних команд.


import java.util.UUID;

public class OrderCommandHandler {
    private final EventStore eventStore;

    public OrderCommandHandler(EventStore eventStore) {
        this.eventStore = eventStore;
    }

    public void handleCreateOrder(String customerId, String product) {
        String orderId = UUID.randomUUID().toString();
        OrderCreatedEvent event = new OrderCreatedEvent(orderId, customerId, product);
        eventStore.saveEvent(event);
    }

    public void handleUpdateOrderStatus(String orderId, String status) {
        OrderStatusUpdatedEvent event = new OrderStatusUpdatedEvent(orderId, status);
        eventStore.saveEvent(event);
    }
}

Крок 4: Query Layer

Шар запитів обробляє події і готує агреговані дані для читання.


import java.util.HashMap;
import java.util.Map;

public class OrderQueryService {
    private final Map<String, String> orderStatus = new HashMap<>();

    public void applyEvent(Object event) {
        if (event instanceof OrderCreatedEvent created) {
            orderStatus.put(created.getOrderId(), "CREATED");
        } else if (event instanceof OrderStatusUpdatedEvent updated) {
            orderStatus.put(updated.getOrderId(), updated.getStatus());
        }
    }

    public String getOrderStatus(String orderId) {
        return orderStatus.get(orderId);
    }
}

Крок 5: Інтеграція

Тепер об'єднаємо все разом.


public class Main {
    public static void main(String[] args) {
        EventStore eventStore = new EventStore();
        OrderCommandHandler commandHandler = new OrderCommandHandler(eventStore);
        OrderQueryService queryService = new OrderQueryService();

        // Створюємо замовлення
        commandHandler.handleCreateOrder("customer123", "Product A");

        // Оновлюємо статус замовлення
        String orderId = eventStore.getEvents()
                                   .stream()
                                   .filter(event -> event instanceof OrderCreatedEvent)
                                   .map(event -> ((OrderCreatedEvent) event).getOrderId())
                                   .findFirst()
                                   .orElseThrow();

        commandHandler.handleUpdateOrderStatus(orderId, "SHIPPED");

        // Обробляємо події в Query Service
        eventStore.getEvents().forEach(queryService::applyEvent);

        // Отримуємо статус замовлення
        System.out.println("Статус замовлення: " + queryService.getOrderStatus(orderId));
    }
}

Основні виклики і типові помилки

Проблема з продуктивністю

При великій кількості подій програвання всього event log може стати вузьким місцем. Рішення? Використовуй snapshots — збережені проміжні стани.

Складність у налагодженні

Подійно-орієнтовані системи складніше налагоджувати через асинхронний характер. Хороші логи і розподілена трасування (наприклад, Spring Sleuth) врятують життя.

Консистентність даних

Важливо, щоб команди і події оброблялися в правильній послідовності. Інакше буде хаос.


CQRS і Event Sourcing — потужний дует для масштабованих і відмовостійких мікросервісів. Вони дозволяють розділити логіку запису і читання, зберігати повну історію дій і працювати з даними асинхронно. Якщо їх правильно налаштувати, вони стануть твоїми вірними союзниками, а не головним болем.

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