Читати й писати різними шляхами
Бертран Меєр сформулював принцип : метод або змінює стан, або повертає дані, але не робить обидві речі одразу. Грег Янг переніс цю ідею з методів на модель застосунку. ділить код на і , і кожен будується під свою задачу.
Бік запису вантажить агрегат, перевіряє інваріанти й зберігає. Бік читання агрегатів може не знати зовсім: картці каталогу потрібні назва вакансії, компанія й вилка, і їх дає один SQL-запит з join, без мапінгу в доменні обʼєкти. Коли читання стає важким, для нього будують окрему : таблицю чи view, яку оновлюють після запису.
Двох баз, шини подій чи event sourcing CQRS не вимагає. Найпростіша форма живе в одній базі: різні класи для читання й запису над тими самими таблицями. Окреме сховище для читання пришвидшує екрани, але приносить : якийсь час читання показує старі дані.
Два шляхи до однієї таблиці
- Запис (src/Hiring: сценарії й домен) містить: SyncJobsFromSource, Job, JobRepository.
- Читання (src/Hiring/Application/Catalog) містить: JobsController, JobCatalog, CatalogPage.
- SyncJobsFromSource використовує Job.
- SyncJobsFromSource використовує JobRepository.
- JobsController використовує JobCatalog.
- JobCatalog створює CatalogPage.
- JobRepository використовує vacancies.
- JobCatalog використовує vacancies.
- 1Запис іде через агрегат і репозиторій, читання через query-сервіс, який віддає DTO.
- 2Query-сервіс нічого не змінює. Якщо щось треба записати, це окремий метод чи сценарій.
- 3Інтерфейс читання живе в шарі застосунку, реалізація з SQL в інфраструктурі.
- 4Окрему read model вводять тоді, коли запит до основних таблиць уже не встигає.
Бік читання каталогу
Інтерфейс читання каталогу живе в src/Hiring/Application/Catalog, а EloquentJobCatalog в інфраструктурі реалізує його запитами до бази й одразу повертає картки. Пише в ту саму таблицю vacancies JobRepository з агрегатом Job. За тим самим принципом зроблено JobPageQuery для сторінки вакансії, CompanyDirectory, SalaryReportQuery та інші. Окрема read model є одна: щоденний знімок ринку в таблиці market_snapshots, який команда hiring:snapshot рахує за розкладом. Поки що сторінка методології бере з нього лише дату першого знімка, а історію накопичують для графіків трендів. Межа не скрізь чиста: JobPageQuery має метод recordView(), що збільшує лічильник переглядів, тобто запис в інтерфейсі читання.
Як CQRS зазвичай перекручують
Повний CQRS для довідника
Дві бази, шина подій і проєкції для списку міст, який змінюють раз на рік. Кожне нове поле проходить пʼять класів і міграцію в двох місцях.
Натомість: почніть з окремих query-класів над тими самими таблицями. Окреме сховище додавайте, коли основна база не встигає.
Читання через агрегати
Каталог вантажить двадцять агрегатів Job з усіма полями, мапить їх у картки й для кожної дочитує компанію. Сторінка робить десятки запитів.
Натомість: query-сервіс одним запитом з join віддає саме ті поля, які потрібні картці.
Забута затримка read model
Користувач зберіг зміну, а сторінка після редиректу читає проєкцію, яку ще не оновили. Здається, що збереження не спрацювало, і людина натискає ще раз.
Натомість: після власної зміни показуйте дані з боку запису або з відповіді команди. Затримку проєкції покажіть явно, якщо вона помітна.
Чи потрібна вам окрема модель читання
- CQS
- Command Query Separation: метод або команда, що змінює стан, або запит, що повертає дані без побічних ефектів.
- CQRS
- Command Query Responsibility Segregation: окремі моделі для запису й читання в межах застосунку.
- Бік запису
- Сценарії, агрегати й репозиторії. Тут перевіряються правила й живуть транзакції.
- Бік читання
- Query-сервіси, які віддають DTO для екрана. Нічого не змінюють і правил не перевіряють.
- Read model
- Дані, підготовлені саме для читання: окрема таблиця, materialized view чи індекс пошуку. Оновлюються після запису.
- Eventual consistency
- Стан, коли read model трохи відстає від запису й наздоганяє його згодом.
Питання зі співбесід
Команда змінює стан і нічого не повертає, хіба що ID нового запису. Запит повертає дані й нічого не змінює, тому його можна викликати скільки завгодно разів без наслідків.
Ні. Достатньо окремих моделей для запису й читання, які можуть ходити в ті самі таблиці, як каталог вакансій PHP Ukraine. Окреме сховище для читання додають, коли саме читання стає вузьким місцем, і разом з ним погоджуються на затримку оновлення.
Оновлювати її після запису: у тій самій транзакції, обробником доменної події або перерахунком за розкладом, як щоденний знімок ринку в PHP Ukraine. Для подій без втрат використовують outbox, а проєкцію роблять ідемпотентною, щоб її можна було перебудувати з нуля.