Да, внешнее api, я понял, в use case у вас транзакция. С несколькими сохранениями на use case понятно, но тоже нарушает принцип 1 транзакция на запрос ну или всеми нелюбимые саги нужны, чтобы либо все сохранения прошли, либо ни одного
чем анализировать требуемый уровень изоляции для каждого use case
Как раз необходимость анализировать каждый кейс при UoW пока не доказана, у нас во вселенной с ORM у всех все работает без этих анализов, зато с оптимистическими блокировками и обновлениями только изменившихся полей моделей из коробки и не тормозит ничего, переходите на темную сторону)
Там - это при использовании DDD с Serializable транзакцией на весь use case или в Transaction Script? :-)
Там - это без повторения всего use case и даже без изменения модели в памяти (как я процитировал ранее), оба примера модель меняют и весь кейс повторяют
потребоваться дополнительные хаки
в DDD как раз рекомендуют не создавать агрегаты из воздуха, а чтобы один создавал другой, так что всё по канонам)
Но вот что произойдёт если бизнес-логика обновления агрегата будет зависеть от выборки группы каких-то других записей из БД
Так это и есть инвариант (наш предыдущей топик) - всё должно быть в одном агрегате
10 зарегистрированных пользователей получают статус "early adopter"
Например, будет некий агрегат RegistrationManager с единственным полем userCount и оптимистической блокировкой - двое поменяют c 9 на 10, она сработает и только первому даст. Ну и, кстати, встречный вопрос - а как там это работает в этом же кейсе при конфликте:
транзакция как раз "короткая" и ничего помимо группы SQL запросов не выполняет - в ней нет каких-то побочных эффектов (даже изменения моделей в памяти, не говоря уже об отправке запросов во внешние API и т.п.)
Там ведь нельзя будет того же нового user-а сохранить, нужно будет вычитать заново, что их уже 10 и поменять тип в user, или как?
Так везде, где не используется UoW
Лучше уж везде UoW использовать, чем Serializable ставить или каждый use-case анализировать, чтобы нужный уровень выбрать, нет?
Сам по себе DDD-шный подход "начать транзакцию, считать весь агрегат, изменить, сохранить весь агрегат, закоммитить транзакцию"
не встречал такого, это откуда? Обычно транзакции явной вообще нет, UoW - считывается агрегат, модифицируется, транзакция ORM-м только на момент записи в базу изменений происходит.
"короткие" транзакции с намного меньшим шансом на конфликт; возможность автоматически и безопасно повторять отдельные транзакции (но не весь use case) при конфликтах (потому что внутри транзакции никто точно не полезет дёргать внешние API с непонятными побочными эффектами и т.п.)
Посмотрел бы я на этот код с повтором не всего use case при конфликте, откуда там данные исходные берутся и как они мержаться с актуальным состоянием в БД. Звучит как микрооптимизации какие-то, в большинстве кейсов проще целиком повторить
А как именно Вы это "делить по другому" и при этом "не укрупнять" себе представляете?
Возможно я понял, в чём у нас недопонимание: я имею ввиду делить по другому, чтобы исключить сагу, не когда уже мы выделили агрегаты из критерия транзакций, а когда:
за агрегаты мы берём не те сущности, которые есть в бизнесе, а которые нам привычны
Т.е. если, например, мы имеем сущности A, B, C и мы решили, что пусть будет два агрегата AB и C, потому что так привычно, а потом оказалось, что нужна транзакция между B и C, то вместо саги, нужно в агрегат объединить как A и BC (т.к. мы не делили изначально по транзакциям, то A и B могли быть вообще не связаны транзакционностью или бизнесу допустима eventual consistency между ними). Если изначально нужна транзакция между AB, а потом ещё и появилось, что нужно между B и C, то понятно, что только укрупнение транзакции/агрегата в ABC или сага.
Нельзя же используя часть данных другого агрегата создать ещё один агрегат, более "удобный" для конкретного use case.
Нет, это про другое, в контексте выделений агрегата по привычке: вот я говорил про User, вместо Buyer, Reporter: представим два use-case 1) создать Order с констрейнтом "не может быть больше 1 активного заказа на User" 2) при проблемах по заказу отправить Ticket в поддержку (констрейнт "1 активный Ticket на одного User-а"). Если бы мы имели один агрегат User, то нам пришлось бы в него объединять и Order-ы и Ticket-ы (пример условный, сейчас за скобками, почему это всё в одном BC). Если же это Buyer и Reporter, то у каждого было бы по своему списку. Если рассматривать это как пример выше с ABC, где User мог бы быть B, а Buyer и Reporter могли бы быть B1 и B2. Тогда в случае с User-м у нас было бы выход только укрупнение до ABC, а в случае с Buyer и Reporter осталось бы AB1 и CB2.
Я в целом, думаю лучше понял статью (спасибо за ответы), понятно, что в случае Transaction Script (ну или если не накладывать таких жёстких ограничений 1 транзакция - 1 агрегат - см. Single transaction across aggregates versus eventual consistency across aggregates NET-Microservices-Architecture-for-Containerized-NET-Applications), саги не нужны будут. По крайней мере если речь идёт о простых случаях, когда саги могли бы быть из-за инвариантов, а не из-за взаимодействий с внешними сервисами.
Звучит как Transaction Script vs Domain Model внутри Bounded Context. Да это бесспорно, что если получилось так разделить на микросервисы, что между ними саг нет, и внутри Transaction Script вывозит бизнес-логику - здорово, так и нужно делать. Но к сожалению это не всегда так, и есть ситуации когда Domain Model внутри Bounded Context даёт больше, чем проблем приносит (1-2 саги можно и реализовать на 50 других use-case-в)
Предложенное Вами дополнение к правилам определения границ агрегата - не является официально рекомендованным лидерами DDD
Повторю сказанное выше: никаких дополнений я не предлагал и не предлагал укрупнять агрегаты при возникновении use-case-в требующих саг (делить по другому, создавать новые агрегаты под разные use-case вместо использования одного агрегата - да, но не именно укрупнять)
И, нет, фраза "по границе транзакции" подразумевает несколько другое и не противоречит использованию саг.
К совместимостью саг с этим тоже вопросов не было, тут мы про одно, думаю, что лучше стремить их минимуму (желательно 0). А вот про то, что подразумевает фраза "по границе транзакции", я не понял, вы же вроде ранее согласились, что:
Разумеется, основной критерий - это транзакции. Но, как Вы же сами и заметили, тут речь исключительно о "transactional consistency".
или нужны ещё цитаты ссылки? Я вроде только это и утверждал
Ну т.е. иными словами вы предлагаете дополнить правила формирования границ агрегата ещё одним
Нет, своего я не предлагал ничего, я про то основное правило - агрегат = инвариант (в терминах, приведённых ранее). Я тут не про те кейсы, когда бизнес допускает согласование с задержкой, но требует саги (отката) (к сожалению и счастью такого не встречал и что-то не могу придумать), я больше про то, что скорей всего бизнес как раз не устраивает с задержкой (например, с UI команда пришла, в которой нужно два агрегата поменять или ни одного и вернуть ответ/ошибку) и вместо того, чтобы пересмотреть границ агрегатов под новые реалии люди лепят сагу.
Встречал кейсы, когда допускается согласование с задержкой, но там не откат, а именно компенсаторное действие и оно отлично без явных саг тоже решается (user добавил товары в корзину, заказал, при сборке заказа обнаружилось, что нет на складе - склад шлёт заказу событие, заказ переводится в состояние, требующее реакции user-а, user может согласиться и опять отправить на склад). В таких примерах нет ничего сложного и нет, никто не предлагает из-за наличия такого use-case-а объединять склад и заказ в один агрегат (не факт даже, что на складе товара нет по причинам concurrency между разными агрегатами, мог просто потеряться/испортится товар).
Для этого в таких проектах граница транзакции проходит по микросервису (Bounded Context), а не агрегату.
Представим простую ситуацию, без взаимодействия с внешними сервисами. В bounded context три агрегата - A, B и C. Мы их проектируем согласно границам транзакций, и большинство транзакций меняют только один из этих агрегатов. Но потом появляется use-case, который требует соблюдение инварианта между B и С - согласно рекомендациям из этой статьи мы себя не ограничиваем и создаём транзакцию на use-case вместо саги, т.к. B и C в одном микросервисе/bounded context-е. Но если это так, то как контролируется/ограничивается, что какой-то другой use-case над B или C не нарушит инвариант между B и C?
В DDD практически всё прекрасно, за исключением одного тактического паттерна: определения границы транзакций по агрегату. (К сожалению, это ключевой элемент всей “тактической” части DDD. Так что избежать этого паттерна можно только если ограничиться в своём проекте применением “стратегии DDD”, полностью отказавшись от “тактики DDD”.)
Я это понял, как "этот тактический паттерн DDD плох, от него нужно отказаться". На что собственно и возражение - что да, он может быть плох, если целиком DDD не следовать, об этом и авторы сами говорят. А вот если следовать основной рекомендации - выделять агрегат по границе транзакции - и менять границы при необходимости с развитием модели, а не лепить саги на существующие агрегаты, то и проблем с паттерном нет
Саги появляются там, где бизнес допускает согласование с некоторой задержкой.
Не придирки ради: тут может быть два варианта - 1 - когда другие агрегаты просто принимают как данность произошедшее в третьем и меняют своё состояние, 2 - когда другие агрегаты тоже проверяют свои инварианты и при их несоблюдении требуется откатить произошедшее в третьем. Я так понимаю мы обсуждаем (2).
И избавиться от саг в этих случаях можно только избыточно раздувая агрегат
Этого (случая 2) позволяет избежать как раз проектирование границ агрегатов от инвариантов в use-case-х. Раздувание происходит как раз когда за агрегаты мы берём не те сущности, которые есть в бизнесе, а которые нам привычны (у нас может появляться User, тогда как вместо него могли быть несколько Buyer, Reporter; Product, вместо BusketItem, InventoryItem - т.е. один из подходов - разделяем сущности по use-case-м так, чтобы use-case укладывался в транзакцию над агрегатом) Да, иногда возникает ситуация, что так, как мы выделили границы агрегатов у нас только 95% операций выполняются транзакционно (с 1 агрегатом на транзакцию), но те 5% нет никаких проблем корректно реализовать сагами (опять же, вместе с бизнесом, обсуждая каждую деталь, что должно быть, когда такси недоступно, а билет на самолёт и в отель мы купили, можем ли мы откатить покупку билета или нужно уточнить у пользователя. И DDD тут только помогает такие кейсы вычленить и на них сфокусироваться с бизнесом).
А какие вообще альтернативы саге в примере про самолёт, отель, такси? Всё в одной транзакции, будем открытой её держать, пока сервис покупки билетов не ответит?
Так а если часть не сохранится, какая разница transaction script у нас или нет, неконситетность будет в базе же?
Конечно, нет select же в транзакции - нет проблем. Ну и никто даже теоретически пример не смог придумать, почему не сработает и в какой ситуации
Да, внешнее api, я понял, в use case у вас транзакция. С несколькими сохранениями на use case понятно, но тоже нарушает принцип 1 транзакция на запрос ну или всеми нелюбимые саги нужны, чтобы либо все сохранения прошли, либо ни одного
Страдать не приходится, когда I/o есть в use case update-а? Его же нужно как-то вне транзакции разместить, но всё ещё в use case?
Как раз необходимость анализировать каждый кейс при UoW пока не доказана, у нас во вселенной с ORM у всех все работает без этих анализов, зато с оптимистическими блокировками и обновлениями только изменившихся полей моделей из коробки и не тормозит ничего, переходите на темную сторону)
Там - это без повторения всего use case и даже без изменения модели в памяти (как я процитировал ранее), оба примера модель меняют и весь кейс повторяют
в DDD как раз рекомендуют не создавать агрегаты из воздуха, а чтобы один создавал другой, так что всё по канонам)
Так это и есть инвариант (наш предыдущей топик) - всё должно быть в одном агрегате
Например, будет некий агрегат RegistrationManager с единственным полем userCount и оптимистической блокировкой - двое поменяют c 9 на 10, она сработает и только первому даст. Ну и, кстати, встречный вопрос - а как там это работает в этом же кейсе при конфликте:
Там ведь нельзя будет того же нового user-а сохранить, нужно будет вычитать заново, что их уже 10 и поменять тип в user, или как?
Лучше уж везде UoW использовать, чем Serializable ставить или каждый use-case анализировать, чтобы нужный уровень выбрать, нет?
Больше всего тоже смущает (если я правильно понял), что транзакцию открывают уже на SELECT-е, не понятно, с какой целью
не встречал такого, это откуда? Обычно транзакции явной вообще нет, UoW - считывается агрегат, модифицируется, транзакция ORM-м только на момент записи в базу изменений происходит.
теория ясна, пример бы конкретного use case-а, когда это важно, можно будет прикинуть как это решает ORM
не встречал кейсы, где read committed + стандартная работа через orm приводила к проблемам и нужно каждый use case проверять, не приведёте пример?
не, на такое желание смотреть пропало)
640 кбread committed должно хватать всем, что-то уж больно специфичное у Вас, не классический энтерпрайзПосмотрел бы я на этот код с повтором не всего use case при конфликте, откуда там данные исходные берутся и как они мержаться с актуальным состоянием в БД. Звучит как микрооптимизации какие-то, в большинстве кейсов проще целиком повторить
Возможно я понял, в чём у нас недопонимание: я имею ввиду делить по другому, чтобы исключить сагу, не когда уже мы выделили агрегаты из критерия транзакций, а когда:
Т.е. если, например, мы имеем сущности A, B, C и мы решили, что пусть будет два агрегата AB и C, потому что так привычно, а потом оказалось, что нужна транзакция между B и C, то вместо саги, нужно в агрегат объединить как A и BC (т.к. мы не делили изначально по транзакциям, то A и B могли быть вообще не связаны транзакционностью или бизнесу допустима eventual consistency между ними). Если изначально нужна транзакция между AB, а потом ещё и появилось, что нужно между B и C, то понятно, что только укрупнение транзакции/агрегата в ABC или сага.
Нет, это про другое, в контексте выделений агрегата по привычке: вот я говорил про User, вместо Buyer, Reporter: представим два use-case 1) создать Order с констрейнтом "не может быть больше 1 активного заказа на User" 2) при проблемах по заказу отправить Ticket в поддержку (констрейнт "1 активный Ticket на одного User-а"). Если бы мы имели один агрегат User, то нам пришлось бы в него объединять и Order-ы и Ticket-ы (пример условный, сейчас за скобками, почему это всё в одном BC). Если же это Buyer и Reporter, то у каждого было бы по своему списку. Если рассматривать это как пример выше с ABC, где User мог бы быть B, а Buyer и Reporter могли бы быть B1 и B2. Тогда в случае с User-м у нас было бы выход только укрупнение до ABC, а в случае с Buyer и Reporter осталось бы AB1 и CB2.
Я в целом, думаю лучше понял статью (спасибо за ответы), понятно, что в случае Transaction Script (ну или если не накладывать таких жёстких ограничений 1 транзакция - 1 агрегат - см. Single transaction across aggregates versus eventual consistency across aggregates NET-Microservices-Architecture-for-Containerized-NET-Applications), саги не нужны будут. По крайней мере если речь идёт о простых случаях, когда саги могли бы быть из-за инвариантов, а не из-за взаимодействий с внешними сервисами.
Звучит как Transaction Script vs Domain Model внутри Bounded Context. Да это бесспорно, что если получилось так разделить на микросервисы, что между ними саг нет, и внутри Transaction Script вывозит бизнес-логику - здорово, так и нужно делать. Но к сожалению это не всегда так, и есть ситуации когда Domain Model внутри Bounded Context даёт больше, чем проблем приносит (1-2 саги можно и реализовать на 50 других use-case-в)
Повторю сказанное выше: никаких дополнений я не предлагал и не предлагал укрупнять агрегаты при возникновении use-case-в требующих саг (делить по другому, создавать новые агрегаты под разные use-case вместо использования одного агрегата - да, но не именно укрупнять)
К совместимостью саг с этим тоже вопросов не было, тут мы про одно, думаю, что лучше стремить их минимуму (желательно 0). А вот про то, что подразумевает фраза "по границе транзакции", я не понял, вы же вроде ранее согласились, что:
или нужны ещё цитаты ссылки? Я вроде только это и утверждал
Нет, своего я не предлагал ничего, я про то основное правило - агрегат = инвариант (в терминах, приведённых ранее). Я тут не про те кейсы, когда бизнес допускает согласование с задержкой, но требует саги (отката) (к сожалению и счастью такого не встречал и что-то не могу придумать), я больше про то, что скорей всего бизнес как раз не устраивает с задержкой (например, с UI команда пришла, в которой нужно два агрегата поменять или ни одного и вернуть ответ/ошибку) и вместо того, чтобы пересмотреть границ агрегатов под новые реалии люди лепят сагу.
Встречал кейсы, когда допускается согласование с задержкой, но там не откат, а именно компенсаторное действие и оно отлично без явных саг тоже решается (user добавил товары в корзину, заказал, при сборке заказа обнаружилось, что нет на складе - склад шлёт заказу событие, заказ переводится в состояние, требующее реакции user-а, user может согласиться и опять отправить на склад). В таких примерах нет ничего сложного и нет, никто не предлагает из-за наличия такого use-case-а объединять склад и заказ в один агрегат (не факт даже, что на складе товара нет по причинам concurrency между разными агрегатами, мог просто потеряться/испортится товар).
Представим простую ситуацию, без взаимодействия с внешними сервисами. В bounded context три агрегата - A, B и C. Мы их проектируем согласно границам транзакций, и большинство транзакций меняют только один из этих агрегатов. Но потом появляется use-case, который требует соблюдение инварианта между B и С - согласно рекомендациям из этой статьи мы себя не ограничиваем и создаём транзакцию на use-case вместо саги, т.к. B и C в одном микросервисе/bounded context-е. Но если это так, то как контролируется/ограничивается, что какой-то другой use-case над B или C не нарушит инвариант между B и C?
Тут:
Я это понял, как "этот тактический паттерн DDD плох, от него нужно отказаться". На что собственно и возражение - что да, он может быть плох, если целиком DDD не следовать, об этом и авторы сами говорят. А вот если следовать основной рекомендации - выделять агрегат по границе транзакции - и менять границы при необходимости с развитием модели, а не лепить саги на существующие агрегаты, то и проблем с паттерном нет
Не придирки ради: тут может быть два варианта - 1 - когда другие агрегаты просто принимают как данность произошедшее в третьем и меняют своё состояние, 2 - когда другие агрегаты тоже проверяют свои инварианты и при их несоблюдении требуется откатить произошедшее в третьем. Я так понимаю мы обсуждаем (2).
Этого (случая 2) позволяет избежать как раз проектирование границ агрегатов от инвариантов в use-case-х. Раздувание происходит как раз когда за агрегаты мы берём не те сущности, которые есть в бизнесе, а которые нам привычны (у нас может появляться User, тогда как вместо него могли быть несколько Buyer, Reporter; Product, вместо BusketItem, InventoryItem - т.е. один из подходов - разделяем сущности по use-case-м так, чтобы use-case укладывался в транзакцию над агрегатом) Да, иногда возникает ситуация, что так, как мы выделили границы агрегатов у нас только 95% операций выполняются транзакционно (с 1 агрегатом на транзакцию), но те 5% нет никаких проблем корректно реализовать сагами (опять же, вместе с бизнесом, обсуждая каждую деталь, что должно быть, когда такси недоступно, а билет на самолёт и в отель мы купили, можем ли мы откатить покупку билета или нужно уточнить у пользователя. И DDD тут только помогает такие кейсы вычленить и на них сфокусироваться с бизнесом).
А какие вообще альтернативы саге в примере про самолёт, отель, такси? Всё в одной транзакции, будем открытой её держать, пока сервис покупки билетов не ответит?