CQRS (разделение ответственности команд и запросов) – это архитектурный паттерн, предполагающий разделение операций записи (команд) и чтения (запросов) на разные модели данныхhabr.com. Проще говоря, используются разные подходы или даже отдельные структуры данных для обновления состояния и для получения данных, вместо единой модели для всех операций. Эта идея впервые была описана Грегом Янгом, а популяризована Мартином Фаулером, подчеркнувшим, что она может существенно влиять на архитектуру системыhabr.com. Изначально она основывается на принципе Command-Query Separation Бертрана Майера (функция либо изменяет состояние, либо возвращает данные, но не делает одновременно и то и другое), расширяя его на уровень всей системы.

Зачем нужен CQRS и какие задачи он решает

В традиционных приложениях одна и та же модель (например, набор Django-моделей и одна база) обслуживает и чтение, и запись. Это прямолинейно и удобно для простых CRUD-задач. Однако с ростом приложения такой подход сталкивается с рядом проблем:

Объединение всего в одной модели приводит к излишней сложности и компромиссам при разработкеlearn.microsoft.com. CQRS предлагает решение: разделить систему на два интерфейса (и часто две подсистемы) – модель записи и модель чтения. Каждая из них может быть оптимизирована под свою задачу и нагрузку независимоlearn.microsoft.com. В результате достигаются следующие преимущества:

Каждая сторона (чтения и записи) развивается независимо. Можно масштабировать их по отдельности: например, вынести read-модель на отдельный реплицированный сервер базы данных или в поисковый движок (Elasticsearch), который будет оперативно обслуживать сложные фильтры и полнотекстовый поиск, не влияя на транзакции записи. Безопасность тоже упрощается: модель чтения может предоставлять только необходимые для UI поля (скрыв чувствительные данные), а доступ на запись можно оградить дополнительными правами. Таким образом, разделение позволяет повысить производительность, масштабируемость и безопасность приложенияlearn.microsoft.com за счёт независимой оптимизации каждой части.

Однако важно отметить, что CQRS не является «серебряной пулей». За выгоды приходится платить усложнением архитектуры. Мартин Фаулер предупреждает, что в большинстве типовых систем внедрение CQRS добавляет сложность и риски, и применять паттерн стоит там, где он действительно оправданmartinfowler.com. Чаще всего это системы с большими нагрузками, сложной доменной логикой или особыми требованиями к производительности интерфейса. В реальных проектах CQRS нередко используют точечно, для отдельных модулей или микросервисов, наиболее чувствительных к вышеописанным проблемам (в терминологии DDD – в отдельных Bounded Context)softwareengineering.stackexchange.com.

Реализация CQRS в Django (теория и практика)

Как же применить принципы CQRS в практике разработки на Django? Рассмотрим сбалансированный подход – без излишнего усложнения, но разделяя ответственность между командами и запросами. Для примера возьмём упрощённый сервис бронирования авиабилетов.

1. Разделение логики на команды и запросы. В стандартном Django-приложении мы часто видим логику чтения и записи прямо в представлениях (views). Например, класс-наследник View может иметь метод get, который делает Flight.objects.filter(...) для получения списка рейсов, и метод post, который создаёт новую запись бронирования через Booking.objects.create(...). При CQRS мы рефакторим это: выносим логику модификации в отдельные классы или функции-команды, а логику чтения – в отдельные функции-запросы или специальные слои. Таким образом, view становится тонким: он лишь получает данные от слоя запросов или вызывает команду и возвращает результат.

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

# commands.py (слой команд)
from django.db import transaction
from .models import Flight, Booking

class BookFlightCommand:
    def __init__(self, flight_id, user_id):
        self.flight_id = flight_id
        self.user_id = user_id

    def execute(self):
        # Все изменение состояния выполняем atomically
        with transaction.atomic():
            # Блокируем запись рейса на чтение/изменение, чтобы избежать гонок
            flight = Flight.objects.select_for_update().get(id=self.flight_id)
            if flight.available_seats <= 0:
                raise Exception("Нет доступных мест на рейсе.")
            # Создаём бронирование
            booking = Booking.objects.create(
                flight=flight,
                user_id=self.user_id,
                status='BOOKED'
            )
            # Обновляем число доступных мест
            flight.available_seats -= 1
            flight.save()
        return booking

В этом классе BookFlightCommand инкапсулировано всё, что нужно, чтобы изменить состояние: проверка бизнес-правил (наличие мест), обновление нескольких связанных моделей (создание Booking и изменение Flight) и сохранение результатов. Важный момент – команда выполняется в транзакции, гарантируя консистентность (либо все изменения применяться, либо ни одно в случае ошибки).

Теперь представление (контроллер) при получении HTTP-запроса на бронирование может просто вызвать эту команду: