Джерело подій CQRS: перевірка унікальності UserName


76

Візьмемо простий приклад "Реєстрація рахунку", ось такий потік:

  • Відвідайте веб-сайт користувача
  • Натисніть кнопку "Зареєструватися" та заповніть форму, натисніть кнопку "Зберегти"
  • Контролер MVC: Перевірте унікальність UserName, прочитавши з ReadModel
  • ЗареєструватиКоманда: Перевірити унікальність UserName ще раз (ось питання)

Звичайно, ми можемо перевірити унікальність UserName, прочитавши з ReadModel в контролері MVC, щоб покращити продуктивність та зручність роботи користувачів. Однак нам все ще потрібно перевірити унікальність ще раз у RegisterCommand , і, очевидно, ми НЕ повинні отримувати доступ до ReadModel у командах.

Якщо ми не використовуємо джерело подій, ми можемо запитати модель домену, тож це не проблема. Але якщо ми використовуємо джерело подій, ми не можемо запитувати модель домену, то як ми можемо перевірити унікальність UserName у реєстріCommand?

Примітка: Клас користувача має властивість Id, а UserName не є ключовою властивістю класу User. Ми можемо отримати об’єкт домену за ідентифікатором, лише використовуючи джерело подій.

ДО: В вимозі, якщо введене Ім'я користувача вже зайняте, веб-сайт повинен показати повідомлення про помилку "Вибачте, ім'я користувача XXX недоступне" для відвідувача. Неприйнятно показувати повідомлення відвідувачеві, кажучи: "Ми створюємо ваш обліковий запис, зачекайте, результат реєстрації ми надішлемо вам електронною поштою пізніше".

Будь-які ідеї? Дуже дякую!

[ОНОВЛЕННЯ]

Більш складний приклад:

Вимога:

Розміщуючи замовлення, система повинна перевіряти історію замовлень клієнта, якщо він є цінним клієнтом (якщо клієнт робив щонайменше 10 замовлень на місяць за останній рік, він є цінним), ми робимо знижку 10% на замовлення.

Реалізація:

Ми створюємо PlaceOrderCommand, і в команді нам потрібно запитати історію замовлень, щоб перевірити, чи цінний клієнт. Але як ми можемо це зробити? Ми не повинні отримувати доступ до ReadModel за командою! Як сказав Мікаель , ми можемо використовувати компенсуючі команди в прикладі реєстрації облікового запису, але якщо ми також використовуємо це в цьому прикладі впорядкування, це буде занадто складно, і код може бути занадто складним для обслуговування.

Відповіді:


37

Якщо ви перевіряєте ім’я користувача за допомогою моделі зчитування перед тим, як надіслати команду, ми говоримо про вікно стану гонки на пару сотень мілісекунд, де може статися реальний стан гонки, який у моїй системі не обробляється. Це надто малоймовірно, щоб це сталося порівняно з вартістю боротьби з цим.

Однак, якщо ви відчуваєте, що з якихось причин ви маєте з цим впоратися, або якщо просто відчуваєте, що хочете знати, як керувати такою справою, ось один із способів:

Ви не повинні отримувати доступ до моделі читання з обробника команд, а також домену під час використання джерела подій. Однак, що ви могли б зробити, це скористатися службою домену, яка б прослуховувала подію UserRegistered, в якій ви знову отримуєте доступ до моделі зчитування та перевіряла, чи ім'я користувача все ще не є дублікатом. Звичайно, вам потрібно використовувати UserGuid тут, а також ваша модель читання може бути оновлена ​​користувачем, якого ви щойно створили. Якщо виявлено копію, у вас є шанс надіслати компенсаційні команди, такі як зміна імені користувача та повідомлення користувача про те, що ім’я користувача було прийнято.

Це один із підходів до проблеми.

Як ви, мабуть, бачите, неможливо зробити це синхронним способом запит-відповідь. Щоб вирішити цю проблему, ми використовуємо SignalR для оновлення користувацького інтерфейсу щоразу, коли є щось, що ми хочемо надіслати клієнту (якщо він все ще зв’язаний, тобто). Ми робимо так, що дозволяємо веб-клієнтові підписатися на події, які містять інформацію, корисну для клієнта для негайного перегляду.

Оновлення

Для більш складної справи:

Я б сказав, що розміщення замовлення є менш складним, оскільки ви можете використовувати модель читання, щоб з’ясувати, чи цінний клієнт, перед тим, як надіслати команду. Насправді, ви можете запитати, що при завантаженні форми замовлення, оскільки ви, мабуть, хочете показати клієнту, що він отримає знижку 10%, перш ніж розмістити замовлення. Просто додайте знижку до PlaceOrderCommandі, можливо, причину знижки, щоб ви могли відстежувати, чому скорочуєте прибуток.

Але знову ж таки, якщо вам дійсно потрібно розрахувати знижку після того, як замовлення з якихось причин було розміщено, знову скористайтеся послугою домену, яка буде слухати, OrderPlacedEventі команда «компенсація» в цьому випадку, мабуть, буде DiscountOrderCommandчимось. Ця команда вплине на сукупний корінь замовлення, і інформація може бути розповсюджена на ваші моделі читання.

Для випадку дублікатів імені користувача:

Ви можете надіслати ChangeUsernameCommandкомпенсаційну команду від служби домену. Або навіть щось більш конкретне, що описувало б причину зміни імені користувача, що також могло б призвести до створення події, на яку веб-клієнт міг би підписатися, щоб ви могли дозволити користувачеві побачити, що ім’я користувача було дублікатом.

У контексті служби домену я б сказав, що у вас також є можливість використовувати інші засоби для сповіщення користувача, наприклад, надсилання електронного листа, який може бути корисним, оскільки ви не можете знати, чи користувач все ще зв’язаний. Можливо, цю функцію сповіщення може ініціювати сама подія, на яку підписується веб-клієнт.

Що стосується SignalR, я використовую концентратор SignalR, до якого користувачі підключаються при завантаженні певної форми. Я використовую функціонал SignalR Group, що дозволяє мені створити групу, якій я називаю значення керівництва, яке я відправляю в команді. Це може бути userGuid у вашому випадку. Тоді у мене є Eventhandler, який підписується на події, які можуть бути корисними для клієнта, і коли подія надходить, я можу викликати функцію javascript на всіх клієнтах групи SignalR (що в цьому випадку буде лише одним клієнтом, який створює дублікат імені користувача у вашому справа). Я знаю, що це звучить складно, але насправді це не так. У мене все це було налаштовано після обіду. На сторінці SignalR Github є чудові документи та приклади.


Що робити у компенсуючій команді, коли виявляю, що ім’я користувача дублюється? Опублікувати подію SignalR, щоб повідомити клієнту, що ім’я користувача недоступне? (Я не використовував SignalR, мабуть, можуть бути якісь "події?")
Mouhong Lin

1
Я думаю, ми називали це Службою додатків у DDD, але я можу помилитися. А також, послуга домену - це обговорюваний термін у спільноті DDDD / CQRS. Однак вам потрібно щось подібне до того, що вони називають Saga, за винятком того, що вам, мабуть, не потрібні ні стан, ні державна машина. Вам просто потрібно щось, що може реагувати на події, виконувати команди пошуку та відправлення даних. Я називаю їх послугами домену. Коротше кажучи, ви передплачуєте події та надсилаєте команди. Це корисно під час спілкування між сукупними коренями.
Mikael Östberg

1
Слід також зазначити, що мої служби доменів мають зовсім інший процес, відокремлений, наприклад, від моделей читання. Це робить речі, пов’язані з обміном повідомленнями, простішими в роботі, такі як підписки тощо.
Mikael Östberg

1
Це чудова відповідь. Однак я бачу цей коментар багато "Ви не повинні отримувати доступ до моделі читання з обробника команд, а також домену, коли використовуєте джерело подій". Хтось може пояснити, чому це така погана ідея використовувати модель читання з боку команди / домену. Це пункт розділення команд / запитів?
Скотт Коутс

1
Поєднання стану домену та команди повинно бути достатнім для прийняття рішення. Якщо ви вважаєте, що вам потрібно прочитати дані під час обробки команд, принесіть ці дані із собою в команді або збережіть їх у стані домену. І чому? - Магазин для читання в кінцевому підсумку послідовний, можливо, він не відповідає правді. Стан домену - це істина, і команда завершує це. - Якщо ви використовуєте ES, ви можете зберегти команду разом із подією (подіями). Таким чином, ви точно бачите, з якою інформацією ви діяли. - Якщо ви прочитали заздалегідь, ви можете виконати перевірку та збільшити ймовірність успіху своєї команди.
Mikael Östberg

24

Я думаю, що у вас ще не відбулося зміщення мислення до можливої ​​послідовності та характеру пошуку подій. У мене була та сама проблема. Зокрема, я відмовився прийняти, що ви повинні довіряти командам від клієнта, які на вашому прикладі говорять "Розмістіть це замовлення зі знижкою 10%", не підтверджуючи домен, що знижка має продовжуватися. Для мене справді вдарило щось, що мені сказав сам Уді (перевірте коментарі прийнятої відповіді).

В основному я зрозумів, що немає причин не довіряти клієнту; все на стороні зчитування створено з моделі домену, тому немає причин не приймати команди. Що б не було в прочитаній стороні, де сказано, що клієнт має право на знижку, домен розмістив його там.

ДО: В вимозі, якщо введене Ім'я користувача вже зайняте, веб-сайт повинен показати повідомлення про помилку "Вибачте, ім'я користувача XXX недоступне" для відвідувача. Неприйнятно показувати повідомлення, сказати: "Ми створюємо ваш обліковий запис, зачекайте, результат реєстрації ми надішлемо вам електронною поштою пізніше", відвідувачеві.

Якщо ви збираєтесь застосувати джерело подій та остаточну узгодженість, вам доведеться визнати, що іноді неможливо буде негайно відображати повідомлення про помилки після подання команди. З унікальним прикладом імені користувача імовірність цього трапляється настільки мала (враховуючи те, що ви перевіряєте сторону читання перед тим, як відправити команду), не варто надто турбуватися, але для цього сценарію потрібно буде надіслати подальше повідомлення або, можливо, запитати їх для іншого імені користувача під час наступного входу. Чудова річ у цих сценаріях полягає в тому, що це змушує задуматися про цінність бізнесу та про те, що насправді важливо.

ОНОВЛЕННЯ: жовтень 2015 р

Просто хотів додати, що насправді, коли йдеться про веб-сайти, які стоять перед громадськістю, це означає, що електронна пошта вже прийнята, насправді проти найкращих практик безпеки. Натомість реєстрація повинна пройти через успішне інформування користувача про те, що електронний лист із підтвердженням надіслано, але у випадку, коли ім’я користувача існує, електронний лист повинен повідомити їх про це та запропонувати їм увійти в систему або скинути пароль. Хоча це працює лише при використанні електронних адрес як імені користувача, що, на мою думку, є доцільним саме з цієї причини.


3
Відмінне введення. Це розум повинен змінитись, перш ніж система зможе (я не збирався там звучати як Йода).
Mikael Östberg

5
+1 Просто бути справді педантичним тут ... ES & EC - це 2 абсолютно різні речі, і використання одного не повинно означати використання іншого (хоча, в більшості випадків це має сенс). Цілком допустимо використовувати ES без наявності послідовно узгодженої моделі і навпаки.
Джеймс

"В основному я зрозумів, що немає причин не довіряти клієнту" - так, я думаю, це справедливий коментар. Але як можна обробити зовнішній доступ, який може створювати команди? Очевидно, ми не хочемо дозволяти PlaceOrderCommand зі знижкою, яка застосовується автоматично; застосування знижки - це логіка домену, а не те, що ми можемо «довірити» комусь сказати нам застосовувати.
Стівен Дрю,

3
@StephenDrew - Клієнт у цьому контексті просто означає будь-яку одиницю коду, що видає команду. Ви можете (і, можливо, повинні) мати рівень перед командною шиною. Якщо ви робили зовнішню веб-службу, контролер mvc, який робить замовлення, спочатку виконує запит, а потім подає команду. Клієнт тут - ваш контролер.
ryeguy

5
Якщо взяти вашу відповідь близько до серця, це означало б, що вся теорія навколо "Інваріантів", "Бізнес-правил", "Високої інкапсуляції" є абсолютною нісенітницею. Існує занадто багато причин, чому не довіряти інтерфейсу користувача. І зрештою UI не є обов’язковою частиною ... що, якщо UI відсутній?
Крістіан Е.

18

Немає нічого поганого у створенні одразу одразу послідовних моделей читання (наприклад, не через розподілену мережу), які оновлюються в тій самій транзакції, що й команда.

Наявність послідовних моделей зчитування в розподіленій мережі допомагає підтримувати масштабування моделі зчитування для важких систем читання. Але нічого не можна сказати, що ви не можете мати конкретну для домену модель зчитування, яка одразу ж узгоджується.

Модель відразу послідовного читання використовується лише для перевірки даних перед тим, як видавати команду; ніколи не слід використовувати її для безпосереднього відображення прочитаних даних користувачеві (тобто з веб-запиту GET або подібного). Використовуйте для цього послідовні, масштабовані моделі читання.


9

Щодо унікальності, я застосував наступне:

  • Перша команда на зразок "StartUserRegistration". UserAggregate буде створено незалежно від того, є користувач унікальним чи ні, але зі статусом RegistrationRequest.

  • На "UserRegistrationStarted" асинхронне повідомлення буде надіслано до служби без реєстрації "UsernamesRegistry". буде щось на зразок "RegisterName".

  • Служба спробує оновити (без запитів, "скажи не питати") таблицю, яка включатиме унікальне обмеження.

  • У разі успіху служба відповість іншим повідомленням (асинхронно), з певною формою авторизації "UsernameRegistration", вказуючи, що ім'я користувача було успішно зареєстровано. Ви можете включити деякий requestId для відстеження у разі одночасної компетенції (малоймовірно).

  • Видавець вищезазначеного повідомлення тепер має дозвіл на те, що ім’я було зареєстровано само, тому тепер можна безпечно позначити сукупність UserRegistration як успішну. В іншому випадку позначте як викинутий.

Підведенню:

  • Цей підхід не передбачає запитів.

  • Реєстрація користувачів завжди створюється без перевірки.

  • Процес підтвердження передбачав би два асинхронних повідомлення та вставку одного базу даних. Таблиця є не частиною зчитаної моделі, а службою.

  • Нарешті, одна асинхронна команда, яка підтверджує дійсність Користувача.

  • На даний момент денормалізатор може реагувати на подію UserRegistrationConfirmed і створювати модель читання для користувача.


2
Я роблю щось подібне. У моїй системі джерел подій у мене є сукупність UserName. Це AggregateID - Ім'я користувача, якого я хотів би зареєструвати. Я видаю команду, щоб зареєструвати його. Якщо це вже зареєстровано, ми отримуємо подію. Якщо він доступний, то він негайно реєструється, і ми отримуємо подію. Я намагаюся уникати "Служб", оскільки іноді вони відчувають, що в домені є недолік моделювання. Роблячи UserName першим класом, ми моделюємо обмеження в домені.
CPerson

7

Думаю, для таких випадків ми можемо використовувати такий механізм, як "консультативний замок із закінченням терміну дії".

Виконання зразка:

  • Перевірте, чи ім’я користувача існує чи не існує у зрештою послідовній моделі читання
  • Якщо не існує; за допомогою redis-couchbase, як сховище ключових значень або кеш; спробуйте висунути ім'я користувача як ключове поле з деяким закінченням терміну дії.
  • У разі успіху; потім підняти userRegisteredEvent.
  • Якщо будь-яке ім'я користувача існує у моделі читання чи кеш-пам’яті, повідомте відвідувачеві, що ім’я користувача прийнято.

Навіть ви можете використовувати базу даних sql; вставити ім'я користувача як первинний ключ якоїсь таблиці блокування; а потім запланована робота може обробляти закінчення терміну дії.


7

Як і багато інших, впроваджуючи систему на основі подій, ми зіткнулися з проблемою унікальності.

Спочатку я був прихильником дозволу клієнту отримати доступ до сторони запиту перед тим, як надіслати команду, щоб з’ясувати, чи унікальне ім’я користувача. Але потім я переконався, що мати бек-енд, який має нульову перевірку унікальності, - погана ідея. Навіщо взагалі застосовувати що-небудь, коли можливо опублікувати команду, яка може пошкодити систему? Бек-енд повинен перевіряти все, що вводиться, інакше ви відкриті для невідповідних даних.

Що ми зробили, це створили indexтаблицю з боку команди. Наприклад, у простому випадку імені користувача, яке повинно бути унікальним, просто створіть таблицю user_name_index, що містить поля, які повинні бути унікальними. Тепер команда команди може запитувати унікальність імені користувача. Після виконання команди нове ім'я користувача безпечно зберігати в індексі.

Щось подібне також може спрацювати для проблеми зі знижкою замовлення.

Переваги полягають у тому, що ваша внутрішня команда команди належним чином перевіряє всі введені дані, тому неможливо зберегти невідповідні дані.

Недоліком може бути те, що вам потрібен додатковий запит для кожного обмеження унікальності, і ви застосовуєте додаткову складність.


2

Ви розглядали можливість використання "робочого" кешу як свого роду RSVP? Це важко пояснити, оскільки воно працює трохи за цикл, але в основному, коли нове ім’я користувача «заявляється» (тобто команда була створена для його створення), ви поміщаєте ім’я користувача в кеш з коротким терміном дії достатньо довгий, щоб врахувати черговий запит, що проходить через чергу і денормалізований у модель зчитування). Якщо це один екземпляр служби, то в пам'яті, ймовірно, буде працювати, інакше централізуйте його за допомогою Redis чи чогось іншого.

Потім, поки наступний користувач заповнює форму (припускаючи, що є інтерфейс), ви асинхронно перевіряєте модель зчитування на наявність імені користувача та попереджаєте користувача, якщо він уже прийнятий. Коли команда надіслана, ви перевіряєте кеш (не модель зчитування), щоб перевірити запит перед тим, як прийняти команду (до повернення 202); якщо ім'я є в кеш-пам’яті, не приймайте команду, якщо це не так, ви додаєте його в кеш; якщо його додавання не вдається (дублікат ключа, оскільки якийсь інший процес вас перебив), то припустимо, що ім’я взято - тоді відповідь клієнту належним чином. Між цими двома речами, я не думаю, що буде багато можливостей для зіткнення.

Якщо переднього кінця немає, тоді ви можете пропустити пошук асинхронізації або, принаймні, попросити ваш API надати кінцеву точку для його пошуку. Ви насправді не повинні дозволяти клієнту говорити безпосередньо з командною моделлю, і розміщення API перед нею дозволить вам мати API, який виступатиме посередником між командою та хостами зчитування.


2

Мені здається, що, можливо, сукупність тут неправильна.

Загалом, якщо вам потрібно гарантувати, що значення Z, що належить Y, є унікальним у наборі X, тоді використовуйте X як сукупність. Зрештою, X - це місце, де насправді існує інваріант (у X може бути лише один Z).

Іншими словами, ваш інваріант полягає в тому, що ім’я користувача може з’являтися лише один раз у межах усіх користувачів вашої програми (або може бути іншим обсягом, наприклад, в межах Організації тощо). команди "RegisterUser" до цього, тоді ви зможете мати те, що вам потрібно, щоб переконатися, що команда дійсна до збереження події "UserRegistered". (І, звичайно, ви можете потім використати цю подію для створення потрібних проекцій, щоб виконати такі дії, як автентифікація користувача, без необхідності завантажувати весь агрегат "ApplicationUsers".


Саме так ви повинні думати про Агрегати. Призначенням Агрегату є захист від одночасності / невідповідності (Ви повинні гарантувати це за допомогою якогось механізму, щоб він був Агрегатом). Коли ви думаєте про них таким чином, ви також усвідомлюєте вартість захисту інваріанта. У найгіршому випадку у вкрай суперечливій системі всі повідомлення до Агрегату повинні були б серіалізуватися та оброблятися одним процесом. Чи це суперечить масштабу, в якому ви працюєте? Якщо так, то слід переглянути значення інваріанта.
Ендрю Ларссон,

2
Для цього конкретного сценарію з іменами користувачів ви все ще можете досягти унікальності, будучи горизонтально масштабованим. Ви можете розділити свій реєстр імен користувача Агрегати на перших N символів імені користувача. Наприклад, якщо вам доводиться обробляти тисячі одночасних реєстрацій, тоді розділіть перші 3 літери імені користувача. Отже, для реєстрації на ім'я користувача "johnwilger123" ви маєте звернутись із повідомленням до Сукупного екземпляра з ідентифікатором "joh", і він може перевірити свій набір усіх імен користувачів "joh" на унікальність.
Ендрю Ларссон,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.