CQRS Event Sourcing: Validar la unicidad del nombre de usuario


Tomemos un simple ejemplo de "Registro de cuenta", aquí está el flujo:

  • Visita el sitio web del usuario
  • Haga clic en el botón" Registrarse "y rellene el formulario, haga clic en el botón" Guardar "
  • MVC Controller: Valida la unicidad del nombre de usuario leyendo desde ReadModel
  • RegisterCommand: Validar la unicidad del nombre de usuario de nuevo (aquí está la pregunta)

Por supuesto, podemos validar la singularidad del nombre de usuario leyendo desde ReadModel en el controlador MVC para mejorar el rendimiento y experiencia de usuario. Sin embargo, todavía necesitamos validar la unicidad de nuevo en RegisterCommand, y obviamente, NO debemos acceder a ReadModel en Comandos.

Si no utilizamos el aprovisionamiento de eventos, podemos consultar el modelo de dominio, por lo que no es un problema. Pero si estamos usando el aprovisionamiento de eventos, no podemos consultar el modelo de dominio, así que ¿cómo podemos validar la unicidad del nombre de usuario en RegisterCommand?

Aviso: La clase de usuario tiene una propiedad Id, y el nombre de usuario no la propiedad key de la clase User. Solo podemos obtener el objeto de dominio por Id cuando usamos el aprovisionamiento de eventos.

POR cierto: En el requisito, si el nombre de usuario introducido ya está tomado, el sitio web debe mostrar el mensaje de error "Lo sentimos, el nombre de usuario XXX no está disponible" para el visitante. No es aceptable mostrar un mensaje, por ejemplo, "Estamos creando su cuenta, espere, le enviaremos el resultado del registro por correo electrónico más tarde", al visitante.

¿Alguna idea? Muchos ¡Gracias!

[ACTUALIZACIÓN]

Un ejemplo más complejo:

Requisito:

Al realizar un pedido, el sistema debe verificar el historial de pedidos del cliente, si es un cliente valioso (si el cliente realizó al menos 10 pedidos al mes en el último año, es valioso), hacemos un 10% de descuento en el pedido.

Aplicación:

Creamos PlaceOrderCommand, y en el comando, necesitamos consultar el historial de pedidos para ver si el el cliente es valioso. ¿Pero cómo podemos hacer eso? ¡No debemos acceder a ReadModel al mando! Como Mikael dijo, podemos usar comandos de compensación en el ejemplo de registro de cuenta, pero si también lo usamos en este ejemplo de orden, sería demasiado complejo, y el código podría ser demasiado difícil de mantener.

Author: Community, 2012-02-29

7 answers

Si valida el nombre de usuario usando el modelo de lectura antes de enviar el comando, estamos hablando de una ventana de condición de carrera de un par de cientos de milisegundos donde puede ocurrir una condición de carrera real, que en mi sistema no se maneja. Es muy poco probable que suceda en comparación con el costo de tratar con él.

Sin embargo, si sientes que debes manejarlo por alguna razón o si simplemente sientes que quieres saber cómo dominar un caso así, aquí hay una manera:

No deberías acceder el modelo leído desde el controlador de comandos ni el dominio cuando se utiliza el aprovisionamiento de eventos. Sin embargo, lo que podría hacer es usar un servicio de dominio que escuche el evento registrado por el usuario en el que vuelva a acceder al modelo de lectura y verifique si el nombre de usuario aún no es un duplicado. Por supuesto que necesita utilizar el UserGuid aquí, así como su modelo de lectura podría haber sido actualizado con el usuario que acaba de crear. Si se encuentra un duplicado, tiene la oportunidad de enviar comandos de compensación como cambiar el nombre de usuario y notificar al usuario que el nombre fue tomado.

Ese es un enfoque del problema.

Como probablemente puede ver, no es posible hacer esto de manera sincrónica solicitud-respuesta. Para resolver eso, estamos usando SignalR para actualizar la interfaz de usuario cada vez que hay algo que queremos enviar al cliente (si todavía están conectados, es decir). Lo que hacemos es que dejamos que el cliente web se suscriba a eventos que contienen información que es útil para el cliente para ver inmediatamente.

Actualización

Para el caso más complejo:

Diría que la colocación del pedido es menos compleja, ya que puede usar el modelo de lectura para averiguar si el cliente es valioso antes de enviar el comando. En realidad, puede consultar eso cuando cargue el formulario de pedido, ya que probablemente desee mostrarle al cliente que obtendrá el 10% de descuento antes de realizar el pedido. Solo tiene que añadir un descuento a la PlaceOrderCommand y tal vez una razón para la descuento, para que pueda realizar un seguimiento de por qué está reduciendo los beneficios.

Pero, de nuevo, si realmente necesita calcular el descuento después de que el pedido se haya realizado por alguna razón, nuevamente use un servicio de dominio que escuche OrderPlacedEvent y el comando "compensador" en este caso probablemente sería un DiscountOrderCommand o algo así. Ese comando afectaría a la raíz del Agregado de Orden y la información podría propagarse a sus modelos leídos.

Para el nombre de usuario duplicado caso:

Puede enviar un ChangeUsernameCommand como el comando de compensación desde el servicio de dominio. O incluso algo más específico, que describiría la razón por la que el nombre de usuario cambió, lo que también podría resultar en la creación de un evento al que el cliente web podría suscribirse para que pueda dejar que el usuario vea que el nombre de usuario es un duplicado.

En el contexto del servicio de dominio, diría que también tiene la posibilidad de usar otros medios para notificar al usuario, como enviar un correo electrónico que podría ser útil ya que no puede saber si el usuario sigue conectado. Tal vez esa funcionalidad de notificación podría iniciarse por el mismo evento al que se suscribe el cliente web.

Cuando se trata de SignalR, uso un Hub SignalR al que los usuarios se conectan cuando cargan un formulario determinado. Utilizo la funcionalidad de Grupo SignalR que me permite crear un grupo al que nombre el valor del Guid que envío en el comando. Este podría ser el userGuid en tu caso. Entonces yo tener Eventhandler que suscribirse a eventos que podrían ser útiles para el cliente y cuando llega un evento puedo invocar una función javascript en todos los clientes en el Grupo SignalR (que en este caso sería solo el cliente que crea el nombre de usuario duplicado en su caso). Sé que suena complejo, pero en realidad no lo es. Hay excelentes documentos y ejemplos en la página de SignalR Github.

 36
Author: Mikael Östberg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-29 12:46:51

Creo que todavía tienes que tener el cambio de mentalidad hacia la consistencia final y la naturaleza del abastecimiento de eventos. Yo tenía el mismo problema. Específicamente me negué a aceptar que debas confiar en los comandos del cliente que, usando tu ejemplo, dicen "Haz este pedido con un 10% de descuento" sin que el dominio valide que el descuento deba seguir adelante. Una cosa que realmente me impactó fue algo que el propio Udi me dijo (revisa los comentarios de la respuesta aceptada).

Básicamente me di cuenta de que no hay razón para no confiar en el cliente; todo en el lado de lectura se ha producido desde el modelo de dominio, por lo que no hay razón para no aceptar los comandos. Lo que sea en el lado de lectura que dice que el cliente califica para el descuento ha sido puesto allí por el dominio.

POR cierto: En el requisito, si el nombre de usuario introducido ya está tomado, el sitio web debe mostrar el mensaje de error "Lo sentimos, el nombre de usuario XXX no está disponible" para el visitante. No es aceptable mostrar un mensaje, por ejemplo, "Estamos creando su cuenta, espere, le enviaremos el resultado del registro por correo electrónico más tarde", al visitante.

Si va a adoptar el aprovisionamiento de eventos y la consistencia eventual, tendrá que aceptar que a veces no será posible mostrar mensajes de error al instante después de enviar un comando. Con el ejemplo de nombre de usuario único, las posibilidades de que esto suceda son tan escasas (dado que revisa el lado de lectura antes de enviar el comando) no vale la pena preocuparse demasiado, pero una notificación posterior tendría que ser enviada para este escenario, o tal vez pedirles un nombre de usuario diferente la próxima vez que inicien sesión. Lo bueno de estos escenarios es que te hace pensar en el valor del negocio y lo que es realmente importante.

ACTUALIZACIÓN: Oct 2015

Solo quería agregar, que en realidad, en lo que respecta a los sitios web de cara al público, indicar que un correo electrónico ya está tomado es en realidad contra las mejores prácticas de seguridad. En su lugar, el registro debe parecer haber pasado por informar con éxito al usuario que se ha enviado un correo electrónico de verificación, pero en el caso de que exista el nombre de usuario, el correo electrónico debe informarles de esto y pedirles que inicien sesión o restablezcan su contraseña. Aunque esto solo funciona cuando se usan direcciones de correo electrónico como nombre de usuario, lo que creo que es recomendable por esta razón.

 19
Author: David Masters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:03:05

No hay nada malo en crear algunos modelos de lectura inmediatamente consistentes (por ejemplo, no sobre una red distribuida) que se actualizan en la misma transacción que el comando.

El hecho de que los modelos de lectura sean consistentes a través de una red distribuida ayuda a soportar el escalado del modelo de lectura para sistemas de lectura pesados. Pero no hay nada que decir que no puede tener un modelo de lectura específico de dominio que sea inmediatamente consistente.

El modelo de lectura inmediatamente consistente solo se usa para comprobar y recibir datos antes de emitir un comando (realmente es un servicio para el comando), nunca debe usarlo para mostrar directamente los datos de lectura a un usuario (es decir, de una solicitud GET web o similar). Utilice eventualmente modelos de lectura consistentes y escalables para eso.

 11
Author: Gaz_Edge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-24 13:35:15

Al igual que muchos otros, al implementar un sistema basado en fuentes de eventos, nos encontramos con el problema de singularidad.

Al principio era partidario de permitir que el cliente acceda al lado de la consulta antes de enviar un comando para averiguar si un nombre de usuario es único o no. Pero luego llegué a ver que tener un back-end que tiene cero validación de singularidad es una mala idea. ¿Por qué hacer cumplir cualquier cosa cuando es posible publicar un comando que corrompería el sistema ? Un back-end debe validar todos es la entrada de otra cosa que está abierto a los datos inconsistentes.

Lo que hicimos fue crear un index en el lado del comando. Por ejemplo, en el caso simple de un nombre de usuario que necesita ser único, simplemente cree un UserIndex con un campo de nombre de usuario. Ahora el lado del comando puede verificar si un nombre de usuario ya está en el sistema o no. Después de ejecutar el comando, es seguro almacenar el nuevo nombre de usuario en el índice.

Algo así también podría funcionar para el problema de descuento de pedidos.

El las ventajas son que el back-end del comando valida correctamente toda la entrada para que no se puedan almacenar datos inconsistentes.

Un inconveniente podría ser que necesita una consulta adicional para cada restricción de unicidad y está imponiendo complejidad adicional.

 5
Author: Jonas Geiregat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-31 12:41:53

Creo que para tales casos, podemos usar un mecanismo como "bloqueo de aviso con vencimiento".

Ejemplo de ejecución:

  • El nombre de usuario de Check existe o no en el modelo de lectura eventualmente consistente
  • Si no existe; usando un almacenamiento o caché de redis-couchbase como keyvalue; intente insertar el nombre de usuario como campo de clave con algún vencimiento.
  • Si tiene éxito; luego levante userRegisteredEvent.
  • Si el nombre de usuario existe en el modelo de lectura o en el almacenamiento en caché, informe al visitante que nombre de usuario ha tomado.

Incluso puede usar una base de datos sql; insertar nombre de usuario como clave principal de alguna tabla de bloqueo; y luego un trabajo programado puede manejar vencimientos.

 4
Author: Safak Ulusoy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-14 10:23:52

¿Ha considerado usar una caché "de trabajo" como una especie de confirmación de asistencia? Es difícil de explicar porque funciona en un poco de ciclo, pero básicamente, cuando un nuevo nombre de usuario es "reclamado" (es decir, se emitió el comando para crearlo), colocas el nombre de usuario en la caché con un vencimiento corto (lo suficientemente largo como para dar cuenta de otra solicitud que pasa por la cola y se denormaliza en el modelo de lectura). Si se trata de una instancia de servicio, entonces en memoria probablemente funcionaría, de lo contrario centralizarlo con Redis o algo.

Luego, mientras el siguiente usuario está llenando el formulario (suponiendo que haya un front end), usted verifica asíncronamente la disponibilidad del nombre de usuario en el modelo de lectura y alerta al usuario si ya está tomado. Cuando se envía el comando, se verifica la caché (no el modelo leído) para validar la solicitud antes de aceptar el comando (antes de devolver 202); si el nombre está en la caché, no acepte el comando, si no lo está, entonces se agrega a la caché; si se produce un error al agregarlo (duplicate key because some other process beat you to it), luego asume que el nombre es tomado then luego responde al cliente apropiadamente. Entre las dos cosas, no creo que haya muchas oportunidades para una colisión.

Si no hay un front end, puede omitir la búsqueda asincrónica o al menos hacer que su API proporcione el endpoint para buscarlo. Realmente no debería permitir que el cliente hable directamente con el modelo de comandos de todos modos, y colocar una API delante de él le permitiría haga que la API actúe como mediador entre el comando y los hosts de lectura.

 1
Author: Sinaesthetic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-30 19:32:18

Acerca de la unicidad, implementé lo siguiente:

  • Un primer comando como "StartUserRegistration". UserAggregate se crearía sin importar si el usuario es único o no, pero con un estado de RegistrationRequested.

  • En " UserRegistrationStarted "se enviaría un mensaje asíncrono a un servicio sin estado"UsernamesRegistry". sería algo así como "RegisterName".

  • Servicio intentaría actualizar (sin consultas, "tell don't ask") tabla que incluiría una restricción única.

  • Si tiene éxito, service respondería con otro mensaje (asíncrono), con una especie de autorización "UsernameRegistration", indicando que el nombre de usuario se registró correctamente. Puede incluir algún RequestId para realizar un seguimiento en caso de competencia concurrente (improbable).

  • El emisor del mensaje anterior tiene ahora una autorización de que el nombre fue registrado por sí mismo, por lo que ahora puede marcar con seguridad el agregado de registro del usuario como exitoso. De lo contrario, marque como descartado.

Terminando:

  • Este enfoque no implica consultas.

  • El registro de usuario siempre se crearía sin validación.

  • El proceso de confirmación implicaría dos mensajes asíncronos y una inserción de bd. La tabla no es parte de un modelo de lectura, sino de un servicio.

  • Finalmente, un comando asíncrono para confirmar que el Usuario es válido.

  • En este punto, un denormalizador podría reaccionar a un evento UserRegistrationConfirmed y crear un modelo de lectura para el usuario.

 0
Author: Daniel Vasquez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-30 00:48:09