CQRS 이벤트 소싱 : UserName 고유성 확인
간단한 "계정 등록"예를 들어 보겠습니다. 흐름은 다음과 같습니다.
- 사용자 방문 웹 사이트
- "등록"버튼을 클릭하고 양식을 작성하고 "저장"버튼을 클릭합니다.
- MVC 컨트롤러 : ReadModel에서 읽어서 UserName 고유성을 확인합니다.
- RegisterCommand : UserName 고유성을 다시 확인합니다 (여기에 질문이 있습니다).
물론 성능과 사용자 경험을 향상시키기 위해 MVC 컨트롤러의 ReadModel에서 읽어서 UserName 고유성을 검증 할 수 있습니다. 그러나 여전히 RegisterCommand에서 고유성을 다시 확인 해야하며 명령에서 ReadModel에 액세스해서는 안됩니다.
Event Sourcing을 사용하지 않으면 도메인 모델을 쿼리 할 수 있으므로 문제가되지 않습니다. 그러나 Event Sourcing을 사용하는 경우 도메인 모델을 쿼리 할 수 없습니다. 그러면 RegisterCommand에서 UserName 고유성을 어떻게 확인할 수 있습니까?
주의 : User 클래스에는 Id 속성이 있으며 UserName은 User 클래스의 키 속성이 아닙니다. 이벤트 소싱을 사용할 때 Id로만 도메인 개체를 가져올 수 있습니다.
BTW : 요구 사항에서 입력 한 사용자 이름이 이미 사용중인 경우 웹 사이트는 방문자에게 "죄송합니다. 사용자 이름 XXX를 사용할 수 없습니다"라는 오류 메시지를 표시해야합니다. 방문자에게 "귀하의 계정을 생성하고 있습니다. 잠시만 기다려주십시오. 등록 결과를 나중에 이메일로 보내 드리겠습니다."라는 메시지를 방문자에게 표시하는 것은 허용되지 않습니다.
어떤 아이디어? 감사합니다!
[최신 정보]
더 복잡한 예 :
요구 사항 :
주문할 때 시스템은 고객의 주문 내역을 확인해야하며, 그가 가치있는 고객이라면 (고객이 작년에 한 달에 최소 10 개의 주문을했다면 가치가있는 경우) 주문에 대해 10 % 할인을받습니다.
이행:
PlaceOrderCommand를 만들고 명령에서 주문 내역을 쿼리하여 클라이언트가 가치가 있는지 확인해야합니다. 하지만 어떻게 할 수 있습니까? 명령으로 ReadModel에 액세스하면 안됩니다! Mikael이 말했듯 이 계정 등록 예제에서 보상 명령을 사용할 수 있지만이 순서 예제에서도 사용하면 너무 복잡하고 코드를 유지 관리하기가 너무 어려울 수 있습니다.
명령을 보내기 전에 읽기 모델을 사용하여 사용자 이름의 유효성을 검사하는 경우 실제 경쟁 조건이 발생할 수있는 수백 밀리 초의 경쟁 조건 창에 대해 이야기하고 있습니다.이 창은 시스템에서 처리되지 않습니다. 처리 비용에 비해 발생 가능성이 너무 낮습니다.
그러나 어떤 이유로 든 처리해야한다고 생각하거나 그러한 경우를 마스터하는 방법을 알고 싶다고 생각하는 경우 한 가지 방법이 있습니다.
이벤트 소싱을 사용할 때 명령 처리기 또는 도메인에서 읽기 모델에 액세스하면 안됩니다. 그러나 할 수있는 일은 다시 읽기 모델에 액세스하는 UserRegistered 이벤트를 수신하고 사용자 이름이 여전히 중복되지 않는지 확인하는 도메인 서비스를 사용하는 것입니다. 물론 여기에서 UserGuid를 사용해야하며 읽기 모델이 방금 만든 사용자로 업데이트되었을 수 있습니다. 중복이 발견되면 사용자 이름을 변경하고 사용자에게 사용자 이름이 사용되었음을 알리는 것과 같은 보상 명령을 보낼 수 있습니다.
이것이 문제에 대한 한 가지 접근 방식입니다.
보시다시피 동기식 요청-응답 방식으로이를 수행하는 것은 불가능합니다. 이를 해결하기 위해 우리는 SignalR을 사용하여 클라이언트에 푸시하려는 것이있을 때마다 UI를 업데이트합니다 (아직 연결되어있는 경우). 우리가하는 일은 웹 클라이언트가 클라이언트가 즉시 볼 수있는 유용한 정보가 포함 된 이벤트를 구독하도록하는 것입니다.
최신 정보
더 복잡한 경우 :
명령을 보내기 전에 읽기 모델을 사용하여 클라이언트가 가치가 있는지 확인할 수 있기 때문에 주문 배치가 덜 복잡하다고 말할 수 있습니다. 실제로 고객이 주문하기 전에 10 % 할인을받을 수 있음을 고객에게 보여주고 싶기 때문에 주문 양식을로드 할 때 쿼리 할 수 있습니다. 에 할인을 추가하고 할인 PlaceOrderCommand
이유를 추가하면 수익이 감소하는 이유를 추적 할 수 있습니다.
그러나 다시 말하지만, 주문이 어떤 이유로 든 장소가 된 후에 정말로 할인을 계산해야하는 경우 다시 수신 할 도메인 서비스를 사용 OrderPlacedEvent
하고이 경우 "보상"명령은 아마도 DiscountOrderCommand
또는 무언가가 될 것입니다. 이 명령은 Order Aggregate 루트에 영향을 미치며 정보는 읽기 모델에 전파 될 수 있습니다.
사용자 이름이 중복 된 경우 :
ChangeUsernameCommand
도메인 서비스에서 보상 명령으로를 보낼 수 있습니다. 또는 더 구체적인 것은 사용자 이름이 변경된 이유를 설명하고 웹 클라이언트가 구독 할 수있는 이벤트를 생성하여 사용자가 사용자 이름이 중복되었음을 알 수 있도록합니다.
도메인 서비스 컨텍스트에서는 사용자가 아직 연결되어 있는지 알 수 없기 때문에 유용 할 수있는 이메일을 보내는 등 다른 방법을 사용하여 사용자에게 알릴 수도 있습니다. 알림 기능은 웹 클라이언트가 구독하는 것과 동일한 이벤트에 의해 시작될 수 있습니다.
SignalR의 경우 사용자가 특정 양식을로드 할 때 연결하는 SignalR Hub를 사용합니다. 명령에서 보내는 Guid 값의 이름을 지정하는 그룹을 만들 수있는 SignalR 그룹 기능을 사용합니다. 귀하의 경우 userGuid 일 수 있습니다. 그런 다음 클라이언트에 유용 할 수있는 이벤트를 구독하는 Eventhandler가 있으며 이벤트가 도착하면 SignalR 그룹의 모든 클라이언트에서 자바 스크립트 함수를 호출 할 수 있습니다 (이 경우에는 클라이언트에서 중복 된 사용자 이름을 생성하는 클라이언트 만 케이스). 복잡하게 들리지만 실제로는 그렇지 않습니다. 오후에 모든 것을 설정했습니다. SignalR Github 페이지에는 훌륭한 문서와 예제가 있습니다.
최종 일관성 과 이벤트 소싱의 본질에 대한 사고 방식이 아직 전환되지 않은 것 같습니다 . 나는 같은 문제가 있었다. 특히, 도메인이 할인이 진행되어야한다는 것을 확인하지 않고 "10 % 할인으로 주문하십시오"라고 말하는 클라이언트의 명령을 신뢰해야한다는 것을 수락하지 않았습니다. 저에게 정말 충격을 준 한 가지는 Udi 자신이 저에게 말한 것입니다 (수락 된 답변의 의견을 확인하십시오).
기본적으로 나는 클라이언트를 신뢰하지 않을 이유가 없다는 것을 깨달았습니다. 읽기 측의 모든 것은 도메인 모델에서 생성되었으므로 명령을 수락하지 않을 이유가 없습니다. 고객이 할인을받을 자격이 있다고 말하는 내용은 도메인에 의해 배치되었습니다.
BTW : 요구 사항에서 입력 한 사용자 이름이 이미 사용중인 경우 웹 사이트는 방문자에게 "죄송합니다. 사용자 이름 XXX를 사용할 수 없습니다"라는 오류 메시지를 표시해야합니다. 방문자에게 "계정을 만드는 중입니다. 잠시 기다려주십시오. 나중에 이메일을 통해 등록 결과를 보내 드리겠습니다."라는 메시지를 표시하는 것은 허용되지 않습니다.
이벤트 소싱 및 최종 일관성을 채택하려는 경우 때때로 명령을 제출 한 후 즉시 오류 메시지를 표시 할 수 없다는 점을 받아 들여야합니다. 고유 한 사용자 이름 예제를 사용하면 이런 일이 발생할 가능성이 매우 적습니다 (명령을 보내기 전에 읽기 측면을 확인하는 경우). 너무 많이 걱정할 필요는 없지만이 시나리오에 대해 후속 알림을 보내거나 질문 할 수 있습니다. 다음에 로그온 할 때 다른 사용자 이름을 사용합니다. 이러한 시나리오의 가장 큰 장점은 비즈니스 가치와 정말 중요한 것에 대해 생각하게한다는 것입니다.
업데이트 : 2015 년 10 월
Just wanted to add, that in actual fact, where public facing websites are concerned - indicating that an email is already taken is actually against security best practices. Instead, the registration should appear to have gone through successfully informing the user that a verification email has been sent, but in the case where the username exists, the email should inform them of this and prompt them to login or reset their password. Although this only works when using email addresses as the username, which I think is advisable for this reason.
There is nothing wrong with creating some immediately consistent read models (e.g. not over a distributed network) that get updated in the same transaction as the command.
Having read models be eventually consistent over a distributed network helps support scaling of the read model for heavy reading systems. But there's nothing to say you can't have a domain specific read model thats immediately consistent.
The immediately consistent read model is only ever used to check data before issuing a command, you should never use it for directly displaying read data to a user (i.e. from a GET web request or similar). Use eventually consistent, scaleable read models for that.
Like many others when implementing a event sourced based system we encountered the uniqueness problem.
At first I was a supporter of letting the client access the query side before sending a command in order to find out if a username is unique or not. But then I came to see that having a back-end that has zero validation on uniqueness is a bad idea. Why enforce anything at all when it's possible to post a command that would corrupt the system ? A back-end should validate all it's input else you're open for inconsistent data.
What we did was create an index
at the command side. For example, in the simple case of a username that needs to be unique, just create a UserIndex with a username field. Now the command side can check if a username is already in the system or not. After the command has been executed it's safe to store the new username in the index.
Something like that could also work for the Order discount problem.
The benefits are that your command back-end properly validates all input so no inconsistent data could be stored.
A downside might be that you need an extra query for each uniqueness constraint and you are enforcing extra complexity.
I think for such cases, we can use a mechanism like "advisory lock with expiration".
Sample execution:
- Check username exists or not in eventually consistent read model
- If not exists; by using a redis-couchbase like keyvalue storage or cache; try to push the username as key field with some expiration.
- If successful; then raise userRegisteredEvent.
- If either username exists in read model or cache storage, inform visitor that username has taken.
Even you can use an sql database; insert username as a primary key of some lock table; and then a scheduled job can handle expirations.
About uniqueness, I implemented the following:
A first command like "StartUserRegistration". UserAggregate would be created no matter if user is unique or not, but with a status of RegistrationRequested.
On "UserRegistrationStarted" an asynchronous message would be sent to a stateless service "UsernamesRegistry". would be something like "RegisterName".
Service would try to update (no queries, "tell don't ask") table which would include a unique constraint.
If successful, service would reply with another message (asynchronously), with a sort of authorization "UsernameRegistration", stating that username was successfully registered. You can include some requestId to keep track in case of concurrent competence (unlikely).
The issuer of the above message has now an authorization that the name was registered by itself so now can safely mark the UserRegistration aggregate as successful. Otherwise, mark as discarded.
Wrapping up:
This approach involves no queries.
User registration would be always created with no validation.
Process for confirmation would involve two asynchronous messages and one db insertion. The table is not part of a read model, but of a service.
Finally, one asynchronous command to confirm that User is valid.
At this point, a denormaliser could react to a UserRegistrationConfirmed event and create a read model for the user.
Have you considered using a "working" cache as sort of an RSVP? It's hard to explain because it works in a bit of a cycle, but basically, when a new username is "claimed" (that is, the command was issued to create it), you place the username in the cache with a short expiration (long enough to account for another request getting through the queue and denormalized into the read model). If it's one service instance, then in memory would probably work, otherwise centralize it with Redis or something.
Then while the next user is filling out the form (assuming there's a front end), you asynchronously check the read model for availability of the username and alert the user if it's already taken. When the command is submitted, you check the cache (not the read model) in order to validate the request before accepting the command (before returning 202); if the name is in the cache, don't accept the command, if it's not then you add it to the cache; if adding it fails (duplicate key because some other process beat you to it), then assume the name is taken -- then respond to the client appropriately. Between the two things, I don't think there'll be much opportunity for a collision.
If there's no front end, then you can skip the async look up or at least have your API provide the endpoint to look it up. You really shouldn't be allowing the client to speak directly to the command model anyway, and placing an API in front of it would allow you to have the API to act as a mediator between the command and read hosts.
It seems to me that perhaps the aggregate is wrong here.
In general terms, if you need to guarantee that value Z belonging to Y is unique within set X, then use X as the aggregate. X, after all, is where the invariant really exists (only one Z can be in X).
In other words, your invariant is that a username may only appear once within the scope of all of your application's users (or could be a different scope, such as within an Organization, etc.) If you have an aggregate "ApplicationUsers" and send the "RegisterUser" command to that, then you should be able to have what you need in order to ensure that the command is valid prior to storing the "UserRegistered" event. (And, of course, you can then use that event to create the projections you need in order to do things such as authenticate the user without having to load the entire "ApplicationUsers" aggregate.
참고URL : https://stackoverflow.com/questions/9495985/cqrs-event-sourcing-validate-username-uniqueness
'Program Tip' 카테고리의 다른 글
다중 Moq It.Is (0) | 2020.11.18 |
---|---|
System.out.println의 다중 스레드 출력이 인터리브 됨 (0) | 2020.11.18 |
Bash : 첫 번째 명령 줄 인수를 취하고 나머지는 전달 (0) | 2020.11.18 |
Visual Studio 2013에서 SSRS (.rptproj) 파일을 열려면 어떻게하나요? (0) | 2020.11.18 |
함수 인수를 사용한 메서드 체인 (0) | 2020.11.17 |