Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое 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 даёт ряд конкретных преимуществ:
- Масштабируемость: Модели чтения и записи можно масштабировать независимо. Нагрузка на чтение в веб-приложениях обычно на порядки выше нагрузки на запись. В Go это позволяет вынести read-модель на отдельные, более мощные инстансы или даже использовать специализированные базы данных (например, Elasticsearch для сложных поисков), в то время как write-модель остаётся на PostgreSQL или другом transactional store.
- Оптимизация производительности: Read-модель может быть денормализованной и полностью адаптированной под требования UI/API, исключая необходимость в сложных JOIN-запросах. Это позволяет использовать простые и быстрые SELECT-H запросы. Для write-модели можно использовать базы, оптимизированные под запись.
- Упрощение кода и повышение гибкости: Разделение устраняет компромиссы в дизайне единой модели. Команды работают с богатой предметной областью (Domain Model), а запросы — с простыми плоскими структурами. Это делает код чище и легче для понимания и поддержки.
- Естественная поддержка 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 он находит эффективное применение благодаря отличной поддержке конкурентности и простоты создания микросервисов или модулей. Его внедрение оправдано в сложных, высоконагруженных системах, где выгоды от разделения ответственности перевешивают увеличение сложности архитектуры.