Что такое CQRS?

Ответ

CQRS (Command Query Responsibility Segregation) — это архитектурный паттерн, который разделяет модель данных и операции на две отдельные части: Команды (Commands) для изменения состояния и Запросы (Queries) для чтения данных.

Ключевые концепции:

  • Команда (Command): Представляет намерение изменить состояние системы (например, PlaceOrderCommand, UpdateUserEmailCommand). Команда выполняется один раз, не возвращает данных (или возвращает только идентификатор/статус) и обладает побочными эффектами.
  • Запрос (Query): Представляет запрос на получение данных без изменения состояния системы (например, GetUserOrdersQuery). Запросы идемпотентны и не имеют побочных эффектов.

Практическая реализация (псевдокод на C#):

// Command и его обработчик (Write Model)
public class CreateProductCommand
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class CreateProductCommandHandler
{
    private readonly WriteDbContext _dbContext;
    public async Task Handle(CreateProductCommand command)
    {
        var product = new Product { Name = command.Name, Price = command.Price };
        _dbContext.Products.Add(product);
        await _dbContext.SaveChangesAsync();
    }
}

// Query и его обработчик (Read Model)
public class GetProductListQuery { }

public class ProductDto { public string Name; public decimal Price; }

public class GetProductListQueryHandler
{
    private readonly ReadDbContext _dbContext; // Может быть отдельная, денормализованная БД
    public async Task<List<ProductDto>> Handle(GetProductListQuery query)
    {
        return await _dbContext.ProductViews
            .Select(p => new ProductDto { Name = p.Name, Price = p.Price })
            .ToListAsync();
    }
}

Преимущества CQRS:

  • Масштабируемость: Модели чтения и записи можно масштабировать независимо.
  • Гибкость: Можно использовать разные типы хранилищ, оптимизированные под конкретные задачи (SQL для команд, NoSQL или материализованные представления для запросов).
  • Более чистая модель: Разделение ответственности упрощает код и делает его более понятным.

Сложности и когда использовать: Паттерн добавляет сложность (необходимость синхронизации моделей, две модели данных). Его стоит применять в системах с высокой нагрузкой, сложной бизнес-логикой или там, где требования к чтению и записи кардинально различаются.

Ответ 18+ 🔞

Давай разберём эту штуку, а то звучит как диагноз из поликлиники — CQRS, ёпта. Представь, что у тебя в голове не одна, а две абсолютно разные квартиры. В одной ты только жрёшь, срать ходишь и вещи кидаешь — это команды. В другой — только смотришь телевизор, отдыхаешь и ни хрена не трогаешь — это запросы. И дверь между ними наглухо заварена, чтобы не мешать друг другу. Вот и весь принцип, ебать мои старые костыли.

Суть, если по-простому:

  • Команда (Command): Это когда ты что-то делаешь и меняешь состояние мира. Купил пива (BuyBeerCommand) — в холодильнике стало на одну бутылку больше. Сказал жене, что задерживаешься (TellWifeImLateCommand) — уровень её доверия к тебе упал до плинтуса. Команда — это действие с последствиями, и она обычно не болтает лишнего, максимум скажет "ок" или "иди нахуй, не получилось".
  • Запрос (Query): Это когда ты просто смотришь, не трогая. Сколько бутылок в холодильнике? (GetBeerCountQuery). Какое у жены настроение? (CheckWifeMoodQuery). Ты ничего не меняешь, просто получаешь данные. Можно спрашивать хоть сто раз — ответ будет один и тот же (пока кто-то другой командой всё не испортит).

Вот как это выглядит в коде, если представить, что мы делаем интернет-магазин для полупидоров:

// КОМАНДА: Создать новый товар (типа, "добавь в ассортимент эту дичь")
public class CreateProductCommand
{
    public string Name { get; set; } // "Набор 'Манда с ушами'"
    public decimal Price { get; set; } // 9999.99
}

// Обработчик команды. Его дело — ВПИЗДЮРИТЬ данные в систему.
public class CreateProductCommandHandler
{
    private readonly WriteDbContext _dbContext; // База для записи
    public async Task Handle(CreateProductCommand command)
    {
        var product = new Product { Name = command.Name, Price = command.Price };
        _dbContext.Products.Add(product); // Записали!
        await _dbContext.SaveChangesAsync(); // И сохранили, ёпта!
    }
}

// ЗАПРОС: Получить список всех товаров (типа, "покажи, что у тебя есть")
public class GetProductListQuery { } // Сам запрос — пустой, просто намерение.

// DTO (Data Transfer Object) — обёртка для данных на выход, чтоб лишнего не светить.
public class ProductDto { public string Name; public decimal Price; }

// Обработчик запроса. Его дело — ВЫТАЩИТЬ данные, не потревожив муравейник.
public class GetProductListQueryHandler
{
    private readonly ReadDbContext _dbContext; // Это может быть ОТДЕЛЬНАЯ база, заточенная под чтение!
    public async Task<List<ProductDto>> Handle(GetProductListQuery query)
    {
        // Просто берём и отдаём. Никаких изменений, чистое чтение.
        return await _dbContext.ProductViews
            .Select(p => new ProductDto { Name = p.Name, Price = p.Price })
            .ToListAsync();
    }
}

Зачем весь этот цирк? Преимущества:

  • Масштабируемость овердохуища: Модель для записи (где сложная бизнес-логика) можно держать на одном сервере, а модель для чтения (где надо быстро отдавать данные тысячам пользователей) — размножить на двадцать. Они друг другу мешать не будут.
  • Гибкость — хуй с горы: Для записи используешь строгую реляционную БД (PostgreSQL), чтобы целостность данных не нарушить. А для чтения — быструю NoSQL (MongoDB) или даже просто кэш (Redis), где данные уже лежат в удобном, денормализованном виде, чтобы не делать эти ебушки-воробушки с десятью JOIN.
  • Чистота, ядрёна вошь: Код не превращается в спагетти, где в одном методе ты и проверяешь права, и меняешь состояние, и данные готовишь для отображения. Всё разделено. Команды — отдельно, запросы — отдельно. Голова не болит.

Но не всё так радужно, подвохи есть: Паттерн этот — не для любой поделки. Это как купить Камаз, чтобы в магазин за хлебом ездить. Добавляет архитектурной сложности — теперь тебе нужно синхронизировать эти две модели. Записал команду в одну БД — как-то надо обновить данные для чтения в другой. Часто это делают через события (Event Sourcing). А ещё теперь у тебя по сути два разных представления об одних и тех же данных. Доверия ебать ноль, что они всегда будут консистентны.

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