← Назад к вопросам

Какую проблему решает паттерн CQRS?

2.2 Middle🔥 202 комментариев
#Архитектура и микросервисы

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Суть паттерна CQRS (Command Query Responsibility Segregation)

CQRS — это архитектурный паттерн, который решает фундаментальную проблему совмещения операций чтения и записи в единой модели данных, что характерно для классического подхода CRUD. Основная идея — разделение ответственности: операции записи (команды — Commands) и операции чтения (запросы — Queries) используют разные модели и часто разные физические хранилища.

Ключевые проблемы, которые решает CQRS

1. Сложность единой модели в высоконагруженных системах

В традиционных приложениях одна и та же модель (например, сущность Order в ORM) используется и для изменения состояния, и для отображения данных. Это приводит к чрезмерному усложнению модели:

  • На модель накладываются противоречивые требования: валидация и инварианты для записи vs. проекции и производительность для чтения.
  • Запросы часто требуют объединения множества таблиц (JOIN), что замедляет выполнение.
// Классический подход: одна модель для всего
public class Order
{
    public int Id { get; set; }
    public string Status { get; set; } // Для записи: только определённые переходы
    public DateTime CreatedDate { get; set; }
    public List<OrderItem> Items { get; set; } // Для чтения: нужно всегда загружать?
    // ... 20+ полей, нужных только для отчётов
}

2. Проблемы производительности при смешанной нагрузке

Операции записи и чтения имеют разные характеристики:

  • Запись требует транзакционности, согласованности, блокировок.
  • Чтение требует минимального времени отклика, кэширования, масштабирования. При использовании одной базы данных эти требования конфликтуют: тяжелые отчёты блокируют таблицы, замедляя обработку команд.

3. Сложность реализации сложных бизнес-правил

В предметно-ориентированном проектировании (DDD) команды часто соответствуют агрегатам с инвариантами, а запросы — простым проекциям. Их смешение размывает ответственность:

// CQRS подход: разделение моделей
// Модель для записи (Command side)
public class OrderAggregate : AggregateRoot
{
    private OrderAggregate() { } // Для EF Core
    public OrderAggregate(int customerId, List<OrderItem> items)
    {
        // Сложная бизнес-логика валидации
        if (items.Count == 0)
            throw new DomainException("Order must have items");
        // ... инварианты
    }
    
    public void CompleteOrder()
    {
        // Проверка переходов состояния
        if (Status != OrderStatus.Pending)
            throw new DomainException("Only pending orders can be completed");
        // ... логика
    }
}

// Модель для чтения (Query side) - плоский DTO
public class OrderView
{
    public int Id { get; set; }
    public string Status { get; set; }
    public decimal TotalAmount { get; set; } // Вычисленное поле
    public string CustomerName { get; set; } // Денормализированные данные
    // Только данные для отображения, без поведения
}

4. Проблемы масштабирования

С помощью CQRS можно:

  • Масштабировать чтение и запись независимо (разные пулы серверов).
  • Использовать разные типы хранилищ: реляционную БД для записи, документную или колоночную — для чтения.
  • Применять кэширование на стороне чтения без влияния на целостность данных.

5. Упрощение обслуживания и эволюции системы

  • Изменения в интерфейсах чтения (новые отчёты, агрегации) не затрагивают модель записи.
  • Можно оптимизировать схемы хранения под конкретные задачи: нормализованная форма для записи, денормализованная — для чтения.
  • Упрощается миграция данных и реализация аналитических функций.

Типичная архитектура CQRS в C#

// Command сторона
public class CreateOrderCommand : ICommand
{
    public int CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
    private readonly IOrderRepository _repository;
    
    public async Task Handle(CreateOrderCommand command)
    {
        var order = new OrderAggregate(command.CustomerId, command.Items);
        await _repository.SaveAsync(order);
        // Событие OrderCreated публикуется для обновления read-модели
    }
}

// Query сторона
public class GetOrderQuery : IQuery<OrderView>
{
    public int OrderId { get; set; }
}

public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderView>
{
    private readonly IOrderViewRepository _readRepository;
    
    public async Task<OrderView> Handle(GetOrderQuery query)
    {
        // Прямое чтение из оптимизированной read-модели
        return await _readRepository.GetByIdAsync(query.OrderId);
    }
}

// Проектор для синхронизации write и read моделей
public class OrderProjector
{
    public void Project(OrderCreatedEvent @event)
    {
        // Денормализация данных в read-хранилище
        var orderView = new OrderView
        {
            Id = @event.OrderId,
            Status = "Pending",
            TotalAmount = CalculateTotal(@event.Items),
            // ... заполнение из разных источников
        };
        _readRepository.Insert(orderView);
    }
}

Когда стоит применять CQRS?

  • Высоконагруженные системы с разными требованиями к чтению и записи.
  • Сложные бизнес-домены с насыщенной логикой (DDD).
  • Системы с аналитикой и отчётами, требующие денормализованных данных.
  • Микросервисные архитектуры, где разделение ответственности естественно.
  • Системы, требующие аудита изменений (event sourcing часто сочетается с CQRS).

Важные предостережения

CQRS не является серебряной пулей и добавляет:

  • Сложность синхронизации данных между write и read моделями.
  • Задержку eventual consistency (согласованность в конечном счёте).
  • Дополнительные затраты на поддержку двух моделей и их синхронизации.

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