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

Что такое CQRS?

1.8 Middle🔥 91 комментариев
#Микросервисы и архитектура

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

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

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

Что такое CQRS?

CQRS (Command Query Responsibility Segregation) — это архитектурный паттерн, который радикально разделяет модели для операций чтения (Query) и операций изменения данных (Command) в приложении. Вместо использования единой модели данных для всех целей, CQRS предлагает создать две отдельные модели: одну для обработки команд, которые изменяют состояние системы, и другую — для выполнения запросов, которые только читают данные. Это фундаментальное разделение ответственностей позволяет оптимизировать каждую модель под её конкретную задачу, что приводит к повышению производительности, масштабируемости и гибкости системы.

Основные принципы CQRS

  • Разделение моделей: Суть паттерна. Модель команд отвечает за бизнес-логику, валидацию и сохранение данных. Модель запросов оптимизирована исключительно для быстрого и эффективного предоставления данных, часто в форме, готовой для отображения (DTO — Data Transfer Object).
  • Command (Команда): Представляет намерение изменить состояние системы. Команды именуются в повелительном наклонении (например, CreateOrderCommand, UpdateUserAddressCommand). Они не возвращают данные (кроме, возможно, идентификатора или статуса выполнения). Их задача — выполнить изменение и, часто, породить событие (Event).
  • Query (Запрос): Представляет запрос на получение данных без их изменения. Запросы именуются как вопросы (например, GetUserProfileQuery, GetDashboardReportQuery). Они не имеют побочных эффектов и возвращают только данные в нужном представлении.
  • Частое сочетание с Event Sourcing: CQRS идеально сочетается с паттерном Event Sourcing. В этой комбинации модель команд не сохраняет текущее состояние сущности, а сохраняет последовательность событий предметной области (Domain Events), которые привели к этому состоянию. Модель запросов при этом строится на отдельном проектированном представлении (Read Model или Projection), которое обновляется асинхронно в ответ на эти события. Это обеспечивает полное разделение и высокую согласованность в конечном счете (Eventual Consistency).

Преимущества использования CQRS в Go

В контексте Go-разработки применение CQRS даёт ряд конкретных преимуществ:

  1. Масштабируемость: Модели чтения и записи можно масштабировать независимо. Нагрузка на чтение в веб-приложениях обычно на порядки выше нагрузки на запись. В Go это позволяет вынести read-модель на отдельные, более мощные инстансы или даже использовать специализированные базы данных (например, Elasticsearch для сложных поисков), в то время как write-модель остаётся на PostgreSQL или другом transactional store.
  2. Оптимизация производительности: Read-модель может быть денормализованной и полностью адаптированной под требования UI/API, исключая необходимость в сложных JOIN-запросах. Это позволяет использовать простые и быстрые SELECT-H запросы. Для write-модели можно использовать базы, оптимизированные под запись.
  3. Упрощение кода и повышение гибкости: Разделение устраняет компромиссы в дизайне единой модели. Команды работают с богатой предметной областью (Domain Model), а запросы — с простыми плоскими структурами. Это делает код чище и легче для понимания и поддержки.
  4. Естественная поддержка Event-Driven Architecture: Go, с его легковесными горутинами и эффективными каналами, отлично подходит для асинхронной обработки событий. Обработчик команды может публиковать событие в канал или брокер (NATS, Kafka), а множество независимых обработчиков-проекторов (projectors) будут обновлять read-модель конкурентно.

Пример реализации на Go

Рассмотрим упрощенный фрагмент структуры приложения для управления задачами (Task).

// COMMAND SIDE

// Domain Event
type TaskCompletedEvent struct {
    TaskID    uuid.UUID
    CompletedAt time.Time
}

// Command
type CompleteTaskCommand struct {
    TaskID uuid.UUID
    UserID uuid.UUID
}

// Command Handler
type CompleteTaskHandler struct {
    eventRepo EventRepository // Сохраняет события
    eventBus  EventBus        // Шина событий
}

func (h *CompleteTaskHandler) Handle(ctx context.Context, cmd CompleteTaskCommand) error {
    // 1. Загружаем историю событий для TaskID (Event Sourcing)
    events, err := h.eventRepo.Load(ctx, cmd.TaskID)
    if err != nil {
        return err
    }
    // 2. Воссоздаём агрегат (Task) из событий, применяем бизнес-логику
    task := domain.ReplayTask(events)
    err = task.Complete(cmd.UserID)
    if err != nil {
        return err // например, задача уже завершена
    }
    // 3. Генерируем новое событие
    newEvent := &TaskCompletedEvent{
        TaskID:    cmd.TaskID,
        CompletedAt: time.Now(),
    }
    // 4. Сохраняем событие и публикуем его
    if err := h.eventRepo.Save(ctx, newEvent); err != nil {
        return err
    }
    h.eventBus.Publish(newEvent)
    return nil
}

// QUERY SIDE

// Read Model (DTO)
type TaskListView struct {
    ID          uuid.UUID `json:"id"`
    Title       string    `json:"title"`
    IsCompleted bool      `json:"is_completed"`
    CompletedAt *time.Time `json:"completed_at,omitempty"`
}

// Query
type GetIncompleteTasksQuery struct {
    UserID uuid.UUID
}

// Query Handler
type GetIncompleteTasksHandler struct {
    readDB *sql.DB // Отдельная БД/схема для чтения
}

func (h *GetIncompleteTasksHandler) Handle(ctx context.Context, query GetIncompleteTasksQuery) ([]TaskListView, error) {
    const sqlStmt = `SELECT id, title, is_completed, completed_at FROM task_views WHERE user_id = $1 AND is_completed = false`
    rows, err := h.readDB.QueryContext(ctx, sqlStmt, query.UserID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var tasks []TaskListView
    for rows.Next() {
        var t TaskListView
        if err := rows.Scan(&t.ID, &t.Title, &t.IsCompleted, &t.CompletedAt); err != nil {
            return nil, err
        }
        tasks = append(tasks, t)
    }
    return tasks, nil
}

// PROJECTOR (синхронизирует Read Model)
type TaskProjector struct {
    readDB *sql.DB
}

func (p *TaskProjector) OnTaskCompleted(event *TaskCompletedEvent) error {
    const sqlStmt = `UPDATE task_views SET is_completed = true, completed_at = $2 WHERE id = $1`
    _, err := p.readDB.Exec(sqlStmt, event.TaskID, event.CompletedAt)
    return err
}

Когда стоит и не стоит использовать CQRS

Используйте CQRS, когда:

  • Система имеет высокую нагрузку на чтение, требующую иной схемы данных.
  • Требуется независимое масштабирование операций чтения и записи.
  • Команды сложны, содержат насыщенную бизнес-логику, а запросы должны быть максимально быстрыми.
  • Вы планируете внедрять Event-Driven Architecture или Event Sourcing.

Избегайте CQRS, когда:

  • Приложение простое (CRUD), и нет проблем с производительностью.
  • Нет ресурсов или экспертизы для поддержки двух моделей и асинхронной синхронизации.
  • Требуется строгая согласованность данных (Strong Consistency) в реальном времени, а не eventual consistency.

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