JavaRush /Курси /Модуль 5. Spring /Лекція 218: Практика: проєктування системи з розділенням ...

Лекція 218: Практика: проєктування системи з розділенням команд і запитів

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

Сьогодні будемо проєктувати реальну систему, застосовуючи підхід CQRS. Пройдемо всі етапи — від постановки задачі до реалізації ключових компонентів. Уявіть, що ми робимо спрощену систему керування замовленнями, щоб клієнти могли зробити замовлення, а менеджери — переглядати список замовлень. Пора зануритись у процес!


Постановка задачі

Ми розробляємо мікросервіс для керування замовленнями в інтернет-магазині. Вимоги такі:

  1. Користувачі можуть створювати замовлення.
  2. Адміністратори та менеджери можуть переглядати список замовлень, фільтрувати замовлення за статусом та отримувати інші звіти.
  3. Робота з даними має бути максимально ефективною.

Вимоги

  • Має бути чіткий розподіл операцій читання і запису.
  • Команди (зміни даних) і запити (читання даних) не повинні заважати одне одному. Наприклад, тривалі операції читання звітів не повинні блокувати створення нових замовлень.
  • Система має бути готова до масштабування, з можливістю обробляти великий обсяг користувачів.

Проєктування

Архітектура з CQRS

Спочатку розділимо відповідальність. Виділяємо дві моделі:

  1. Командна модель (Write Model): відповідає за зміну стану системи (наприклад, створення та оновлення замовлень).
  2. Запитна модель (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. Запитна модель:

Для запитів дані оптимізуються заздалегідь. Наприклад, робимо плоску таблицю, яка одразу містить всю інформацію про замовлення, щоб не робити складних join-ів.

Приклад моделі:


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();
}

Примітки та покращення

  1. Синхронізація даних між Write і Read моделями:
    • При оновленні даних у Write Database можна використовувати події (наприклад, Kafka), щоб оновлювати Read Database.
  2. Вибір Read Database:
    • У простих сценаріях можна використовувати ту ж базу даних (але з окремими таблицями). У складних випадках для запитів можна застосувати NoSQL бази даних, такі як Elasticsearch.
  3. Кешування:
    • Для прискорення запитів можна використовувати кеш, наприклад Redis.
  4. Обробка помилок:
    • Обов'язково обробіть помилки: наприклад, валідацію даних, недоступність бази даних тощо.

Отже, завдяки CQRS ми розділили операції зміни та читання даних, що покращує продуктивність і масштабованість системи. У реальних проєктах підхід CQRS дозволяє впоратись з високими навантаженнями і складністю обробки даних, даючи більш керовану архітектуру.

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