CQRS (разделение ответственности команд и запросов) – это архитектурный паттерн, предполагающий разделение операций записи (команд) и чтения (запросов) на разные модели данныхhabr.com. Проще говоря, используются разные подходы или даже отдельные структуры данных для обновления состояния и для получения данных, вместо единой модели для всех операций. Эта идея впервые была описана Грегом Янгом, а популяризована Мартином Фаулером, подчеркнувшим, что она может существенно влиять на архитектуру системыhabr.com. Изначально она основывается на принципе Command-Query Separation Бертрана Майера (функция либо изменяет состояние, либо возвращает данные, но не делает одновременно и то и другое), расширяя его на уровень всей системы.
В традиционных приложениях одна и та же модель (например, набор Django-моделей и одна база) обслуживает и чтение, и запись. Это прямолинейно и удобно для простых CRUD-задач. Однако с ростом приложения такой подход сталкивается с рядом проблем:
Объединение всего в одной модели приводит к излишней сложности и компромиссам при разработкеlearn.microsoft.com. CQRS предлагает решение: разделить систему на два интерфейса (и часто две подсистемы) – модель записи и модель чтения. Каждая из них может быть оптимизирована под свою задачу и нагрузку независимоlearn.microsoft.com. В результате достигаются следующие преимущества:
ЗабронироватьРейс
будет проверять наличие мест, создавать бронирование и уменьшать счётчик свободных мест – и всё это в одном транзакционном действии. Команда выражается на языке предметной области, отражая намерение пользователя: например, «Забронировать номер в отеле», а не просто «установить поле ReservationStatus = Reserved»learn.microsoft.com. Такой подход ближе к бизнес-процессам и облегчает понимание намерения команды.Каждая сторона (чтения и записи) развивается независимо. Можно масштабировать их по отдельности: например, вынести read-модель на отдельный реплицированный сервер базы данных или в поисковый движок (Elasticsearch), который будет оперативно обслуживать сложные фильтры и полнотекстовый поиск, не влияя на транзакции записи. Безопасность тоже упрощается: модель чтения может предоставлять только необходимые для UI поля (скрыв чувствительные данные), а доступ на запись можно оградить дополнительными правами. Таким образом, разделение позволяет повысить производительность, масштабируемость и безопасность приложенияlearn.microsoft.com за счёт независимой оптимизации каждой части.
Однако важно отметить, что CQRS не является «серебряной пулей». За выгоды приходится платить усложнением архитектуры. Мартин Фаулер предупреждает, что в большинстве типовых систем внедрение CQRS добавляет сложность и риски, и применять паттерн стоит там, где он действительно оправданmartinfowler.com. Чаще всего это системы с большими нагрузками, сложной доменной логикой или особыми требованиями к производительности интерфейса. В реальных проектах CQRS нередко используют точечно, для отдельных модулей или микросервисов, наиболее чувствительных к вышеописанным проблемам (в терминологии DDD – в отдельных Bounded Context)softwareengineering.stackexchange.com.
Как же применить принципы 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-запроса на бронирование может просто вызвать эту команду: