Garantizar la coherencia transaccional con DDD

9

Estoy comenzando con DDD y entiendo que las raíces agregadas se utilizan para garantizar la coherencia transnacional. No debemos modificar múltiples agregados en un servicio de aplicación.

Sin embargo, me gustaría saber cómo lidiar con la siguiente situación.

Tengo una raíz agregada llamada Productos.

También hay una raíz agregada llamada Grupo.

Ambos tienen Id, y se pueden editar de forma independiente.

Varios productos pueden apuntar al mismo grupo.

Tengo un servicio de aplicación que puede cambiar el grupo de un producto:

ProductService.ChangeProductGroup(string productId, string groupId)

  1. Comprobar grupo existe
  2. Obtener producto del repositorio
  3. Establecer su grupo
  4. Escriba el producto nuevamente en el repositorio

También tengo un servicio de aplicación donde se puede eliminar el grupo:

GroupService.DeleteGroup(string groupId) 1. Obtenga productos del repositorio cuyo groupId esté configurado en groupId proporcionado, asegúrese de que el recuento sea 0 o cancele 2. Elimine el grupo del repositorio de grupos 3. Guarde los cambios

Mi pregunta es el siguiente escenario, ¿qué pasaría si:

En ProductService.ChangeProductGroup, verificamos que el grupo existe (sí existe), luego, justo después de esta verificación, un usuario separado elimina el productGroup (a través del otro GroupService.DeleteGroup). En este caso, establecemos una referencia a un producto que acaba de eliminarse.

¿Es esta una falla en mi diseño que debería usar un diseño de dominio diferente (agregando elementos adicionales si es necesario), o tendría que usar transacciones?

morleyc
fuente

Respuestas:

2

Tenemos el mismo problema.

Y no veo forma de resolver este problema, sino con las transacciones o con la comprobación de coherencia en la base de datos para obtener una excepción en el peor de los casos.

También puede usar el bloqueo pesimista, bloqueando la raíz agregada o solo sus partes para otros clientes hasta que se complete la transacción comercial, lo que en cierta medida es equivalente a la transacción serializable.

La forma en que va en gran medida depende de su sistema y lógica empresarial.
La concurrencia no es una tarea fácil en absoluto.
Incluso si puede detectar un problema, ¿cómo lo resolverá? ¿Solo cancela la operación o permite al usuario "fusionar" los cambios?

Usamos Entity Framework y EF6 usa read_commited_snapshot por defecto, por lo que dos lecturas consecutivas del repositorio pueden darnos datos inconsistentes. Solo tenemos esto en cuenta para el futuro, cuando los procesos comerciales se describirán con mayor claridad y podamos tomar decisiones informadas. Y sí, seguimos verificando la consistencia a nivel de modelo como lo hace. Esto al menos permite probar BL por separado de DB.

También le sugiero que piense en la coherencia del repositorio en caso de que tenga transacciones comerciales 'largas'. Resultó bastante complicado.

Pavel Voronin
fuente
1

Creo que esta no es una pregunta específica del diseño impulsado por el dominio, sino una cuestión de gestión de recursos.

Los recursos simplemente deben bloquearse a medida que se adquieren .

Esto es cierto cuando el servicio de aplicación sabe de antemano que el recurso adquirido (el Groupagregado, en su ejemplo) se va a modificar. En el diseño controlado por dominio, el bloqueo de recursos es un aspecto que pertenece al repositorio.

rucamzu
fuente
No, el bloqueo no es un "deber". De hecho, es algo bastante horrible que hacer con las transacciones de larga duración. Es mejor usar la simultaneidad optimista, que todo ORM moderno admite de forma inmediata. Y lo mejor de la concurrencia optimista es que también funciona con bases de datos no transaccionales, siempre que admitan actualizaciones atómicas.
Aaronaught
1
Estoy de acuerdo con los inconvenientes de encerrar dentro de las transacciones de larga duración, pero el escenario descrito por @ g18c sugiere todo lo contrario, es decir, operaciones breves sobre agregados individuales.
rucamzu
0

¿Por qué no almacena los identificadores de producto en la entidad del Grupo? De esta manera, solo se trata de un único agregado que facilitará las cosas.

Luego deberá implementar algún tipo de patrón de concurrencia, por ejemplo, si elige concurrencia optimista, simplemente agregue una propiedad de versión a la entidad del Grupo y arroje una excepción si las versiones no coinciden al actualizar, es decir

Agregar producto a un grupo

  1. Verifique si el producto existe
  2. Obtener grupo del repositorio (incluida su propiedad de ID de versión)
  3. Group.Add (ProductId)
  4. Guarde el grupo usando el repositorio (actualice con una nueva identificación de versión y agregue una cláusula where para asegurarse de que la versión no haya cambiado)

Muchos ORM tienen una simultaneidad optimista incorporada usando identificadores de versión o marcas de tiempo, pero es fácil de usar. Aquí hay una buena publicación sobre cómo se hace en Nhibernate http://ayende.com/blog/3946/nhibernate-mapping-concurrency .

Scott Millett
fuente
0

Aunque las transacciones pueden ayudar, hay otra forma, especialmente si su escenario se limita a unos pocos casos.

Podría incluir comprobaciones en las consultas que realizan las mutaciones, haciendo que cada par de comprobación y mutación sea una operación atómica.

La consulta de actualización del producto interna se une al grupo (recién referenciado). Esto hace que no actualice nada si ese grupo se ha ido.

La consulta de eliminación de grupo une todos los productos que apuntan a ella y tiene la condición adicional de que (cualquier columna de) el resultado de la unión debe ser nulo. Esto hace que no elimine nada si algún producto lo señala.

Las consultas devuelven el número de filas que coincidieron o se actualizaron. Si el resultado es 0, entonces perdió una condición de carrera y no hizo nada. Puede lanzar una excepción, y la capa de aplicación puede manejar eso como quiera, como al intentar nuevamente desde el principio.

Timo
fuente