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

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

Модуль 5. Spring
22 уровень , 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. Запросная модель:

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

Пример модели:


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 позволяет справляться с высокими нагрузками и сложностями обработки данных, предоставляя более управляемую архитектуру.

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