driven domain ddd context domain-driven-design cqrs event-sourcing unique

domain driven design - domain - Aprovisionamiento de eventos de CQRS: validar la exclusividad de UserName



ddd context (7)

¿Has considerado usar un caché "de trabajo" como una especie de RSVP? Es difícil de explicar porque funciona en un ciclo, pero básicamente, cuando un nuevo nombre de usuario es "reclamado" (es decir, el comando fue emitido para crearlo), usted coloca el nombre de usuario en el caché con una corta caducidad ( el tiempo suficiente para dar cuenta de otra solicitud que pasa por la cola y se desnormaliza en el modelo de lectura). Si es una instancia de servicio, entonces en la memoria probablemente funcione, de lo contrario centralícela con Redis o algo así.

Luego, mientras el próximo usuario completa el formulario (suponiendo que haya una interfaz), usted verifica asincrónicamente el modelo de lectura para la disponibilidad del nombre de usuario y alerta al usuario si ya se tomó. Cuando se envía el comando, se verifica el caché (no el modelo de lectura) para validar la solicitud antes de aceptar el comando (antes de devolver 202); si el nombre está en la memoria caché, no acepte el comando, si no lo está, entonces agréguelo al caché; si agrega que falla (clave duplicada porque algún otro proceso lo golpea), entonces suponga que se toma el nombre, luego responda al cliente de manera apropiada. Entre las dos cosas, no creo que haya muchas oportunidades para una colisión.

Si no hay una interfaz, puede omitir la búsqueda asincrónica o al menos tener su API para que el punto final pueda buscarla. En realidad, no debería permitir que el cliente hable directamente al modelo de comando, y colocar una API frente a él le permitiría tener la API para actuar como un mediador entre el comando y los hosts leídos.

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

  • Sitio web de visita del usuario
  • Haga clic en el botón "Registrar" y complete el formulario, haga clic en el botón "Guardar"
  • Controlador MVC: valide la exclusividad del nombre de usuario leyendo ReadModel
  • RegisterCommand: valide la exclusividad de UserName nuevamente (aquí está la pregunta)

Por supuesto, podemos validar la exclusividad de UserName leyendo ReadModel en el controlador MVC para mejorar el rendimiento y la experiencia del usuario. Sin embargo, todavía necesitamos validar la singularidad nuevamente en RegisterCommand , y obviamente, NO debemos acceder a ReadModel en Commands.

Si no utilizamos Event Sourcing, podemos consultar el modelo de dominio, por lo que no hay problema. Pero si utilizamos Event Sourcing, no podemos consultar el modelo de dominio, entonces, ¿cómo podemos validar la exclusividad UserName en RegisterCommand?

Aviso: la clase de usuario tiene una propiedad Id, y UserName no es la propiedad clave de la clase User. Solo podemos obtener el objeto de dominio por Id al usar el abastecimiento de eventos.

Por cierto: en el requisito, si el Nombre de usuario ingresado 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 de registro por correo electrónico más tarde" al visitante.

¿Algunas ideas? ¡Muchas gracias!

[ACTUALIZAR]

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 por mes en el último año, es valioso), hacemos un 10% de descuento en el pedido.

Implementación:

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


Al igual que muchos otros cuando implementamos un sistema basado en eventos, encontramos el problema de exclusividad.

Al principio, yo 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 me di cuenta de que tener un back-end que tenga cero validación en la unicidad es una mala idea. ¿Por qué imponer algo cuando es posible publicar un comando que podría dañar el sistema? Un back-end debe validar toda su entrada, de lo contrario, está abierto para 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 debe 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 que se haya ejecutado 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 la orden.

Los beneficios son que el back-end de su comando valida correctamente todas las entradas para que no se almacenen datos incoherentes.

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


Creo que aún debe cambiar la mentalidad a la consistencia final y la naturaleza del evento. Yo tuve el mismo problema. Específicamente, me negué a aceptar que debe confiar en los comandos del cliente que, utilizando su ejemplo, diga "Realizar este pedido con 10% de descuento" sin que el dominio valide que el descuento debe continuar. Una cosa que realmente me impactó fue algo que el mismo Udi me dijo (verifique 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 a partir del modelo de dominio, por lo que no hay ninguna razón para no aceptar los comandos. Cualquier cosa 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 ingresado 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 de registro por correo electrónico más tarde" al visitante.

Si va a adoptar el abastecimiento de eventos y la coherencia final, tendrá que aceptar que a veces no será posible mostrar mensajes de error al instante después de enviar un comando. Con el único ejemplo de nombre de usuario, las posibilidades de que esto ocurra son tan escasas (dado que revisa el lado de lectura antes de enviar el comando) no vale la pena preocuparse por demasiado, pero debería enviarse una notificación posterior para este escenario, o quizás preguntar para 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: octubre de 2015

Solo quería agregar que, en realidad, en lo que respecta a los sitios web que se enfrentan al público, lo que indica que un correo electrónico ya está tomado está en realidad en contra de las mejores prácticas de seguridad. En cambio, parece que el registro se realizó satisfactoriamente informando al usuario de que se envió un correo electrónico de verificación, pero en el caso de que exista el nombre de usuario, el correo electrónico debe informarlo e indicarle que inicie sesión o restablezca su contraseña. Aunque esto solo funciona cuando utilizo direcciones de correo electrónico como nombre de usuario, lo que creo que es aconsejable por este motivo.


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

Ejecución de muestra:

  • Compruebe si el nombre de usuario existe o no en el modelo de lectura consistente al final
  • Si no existe; mediante el uso de un redis-couchbase como almacenamiento de clave de almacenamiento o caché; intenta presionar el nombre de usuario como campo clave con algún vencimiento.
  • Si tiene éxito; luego elevar userRegisteredEvent.
  • Si el nombre de usuario existe en el modelo de lectura o en el almacenamiento en caché, informe al visitante que el nombre de usuario lo tomó.

Incluso puedes 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 expiraciones.


No hay nada de malo en crear algunos modelos de lectura inmediatamente consistentes (p. Ej., No en una red distribuida) que se actualicen en la misma transacción que el comando.

Tener modelos de lectura eventualmente 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 se puede tener un modelo de lectura específico de dominio que sea inmediatamente coherente.

El modelo de lectura inmediatamente consistente solo se usa para verificar y recibir datos antes de emitir un comando (realmente es un servicio al comando), nunca debe usarlo para mostrar directamente datos leídos a un usuario (es decir, desde una solicitud web GET o similar ) Use modelos de lectura escalables y eventualmente consensuados para eso.


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 lidiar con él.

Sin embargo, si sientes que debes manejarlo por alguna razón o si sientes que deseas saber cómo dominar tal caso, aquí hay una forma:

No debe acceder al modelo de lectura desde el controlador de comando ni desde el dominio al usar el abastecimiento de eventos. Sin embargo, lo que podría hacer es usar un servicio de dominio que escuche el evento UserRegistered en el que accede nuevamente al modelo de lectura y verifique si el nombre de usuario aún no es un duplicado. Por supuesto, debe 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 posibilidad de enviar comandos de compensación, como cambiar el nombre de usuario y notificar al usuario que se tomó el nombre de usuario.

Ese es un enfoque al problema.

Como probablemente pueda ver, no es posible hacerlo de forma síncrona de solicitud y respuesta. Para resolver eso, estamos usando SignalR para actualizar la IU cada vez que hay algo que queremos presionar al cliente (si todavía están conectados, eso es). Lo que hacemos es que permitamos que el cliente web se suscriba a eventos que contienen información que es útil para que el cliente vea de inmediato.

Actualizar

Para el caso más complejo:

Diría que la ubicación de la orden 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, podría consultarlo cuando cargue el formulario de pedido, ya que probablemente quiera mostrarle al cliente que obtendrán el 10% de descuento antes de realizar el pedido. Simplemente agregue un descuento a PlaceOrderCommand y quizás una razón para el descuento, para que pueda rastrear por qué está recortando ganancias.

Pero, de nuevo, si realmente necesita calcular el descuento después de que el pedido se haya realizado por algún motivo, nuevamente use un servicio de dominio que escuche OrderPlacedEvent y el comando "compensar" en este caso probablemente sea un comando DiscountOrderCommand o algo así. Ese comando afectaría a la raíz del Agregado de la orden y la información podría propagarse a sus modelos de lectura.

Para el caso de nombre de usuario duplicado:

Puede enviar un ChangeUsernameCommand como el comando de compensación del servicio de dominio. O incluso algo más específico, que describiría la razón por la cual 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, de modo que pueda dejar que el usuario vea que el nombre de usuario fue un duplicado.

En el contexto del servicio de dominio, yo diría que también tiene la posibilidad de utilizar 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 todavía está conectado. Tal vez esa funcionalidad de notificación podría iniciarse por el mismo evento al que el cliente web se está suscribiendo.

Cuando se trata de SignalR, uso un SignalR Hub al que los usuarios se conectan cuando cargan un determinado formulario. Utilizo la funcionalidad SignalR Group que me permite crear un grupo al que le pongo el nombre de la Guid que envío en el comando. Este podría ser el userguid en su caso. Luego tengo el manejador de eventos que se suscribe a los eventos que podrían ser útiles para el cliente y cuando llega un evento puedo invocar una función javascript en todos los clientes del grupo SignalR (que en este caso sería el único cliente creando el nombre de usuario duplicado en caso). Sé que suena complejo, pero realmente no lo es. Lo tenía todo preparado en una tarde. Hay excelentes documentos y ejemplos en la página de SignalR Github.


Sobre la singularidad, 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á un mensaje asíncrono a un servicio sin estado "UsernamesRegistry". sería algo así como "RegisterName".

  • El servicio intentaría actualizar (sin consultas, "no preguntar") que incluiría una restricción única.

  • Si tiene éxito, el servicio respondería con otro mensaje (de forma asíncrona), con una especie de autorización "UsernameRegistration", que indica que el nombre de usuario se registró correctamente. Puede incluir algún requestId para realizar un seguimiento en caso de competencia concurrente (poco probable).

  • 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 el agregado de UserRegistration como exitoso. De lo contrario, marque como descartado.

Terminando:

  • Este enfoque no implica consultas.

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

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

  • Finalmente, un comando asincrónico para confirmar que el Usuario es válido.

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