Así que he estado coqueteando con Event Sourcing y CQRS durante un tiempo, aunque nunca tuve la oportunidad de aplicar los patrones en un proyecto real.
Entiendo los beneficios de separar sus inquietudes de lectura y escritura, y aprecio cómo Event Sourcing hace que sea fácil proyectar cambios de estado en las bases de datos de "Modelo de lectura" que son diferentes de su Tienda de eventos.
Lo que no tengo muy claro es por qué alguna vez rehidratarías tus agregados de la tienda de eventos.
Si proyectar cambios en bases de datos "leídas" es tan fácil, ¿por qué no siempre proyectar cambios en una base de datos "escrita" cuyo esquema coincida perfectamente con su modelo de dominio? Esto sería efectivamente una base de datos de instantáneas.
Me imagino que esto debe ser bastante común en las aplicaciones ES + CQRS en la naturaleza.
Si este es el caso, ¿es útil el Almacén de eventos al reconstruir su base de datos de "escritura" como resultado de cambios de esquema? ¿O me estoy perdiendo algo más grande?
fuente
Respuestas:
Porque los "eventos" son el libro de registro.
Si; A veces es una optimización de rendimiento útil utilizar una copia en caché del estado agregado, en lugar de regenerar ese estado desde cero cada vez. Recuerde: la primera regla de optimización del rendimiento es "No". Agrega complejidad adicional a la solución, y preferiría evitarlo hasta que tenga una motivación comercial convincente.
Te estás perdiendo algo más grande.
El primer punto es que si está considerando una solución de origen de eventos, es porque espera que haya valor en preservar el historial de lo que sucedió, es decir que quiere hacer cambios no destructivos .
Por eso estamos escribiendo a la tienda de eventos.
En particular, esto significa que cada cambio debe escribirse en la tienda de eventos.
Los escritores competidores podrían potencialmente destruir las escrituras de los demás o conducir el sistema a un estado no deseado, si no están al tanto de las ediciones de los demás. Entonces, el enfoque habitual cuando necesita consistencia es dirigir sus escritos a una posición específica en el diario (análoga a un PUT condicional en una API HTTP). Una escritura fallida le dice al escritor que su comprensión actual de la revista no está sincronizada y que deberían recuperarse.
Volver a una buena posición conocida y luego reproducir cualquier evento adicional desde ese punto es una estrategia de recuperación común. Esa buena posición conocida puede ser una copia de lo que está en el caché local o una representación en su tienda de instantáneas.
En el camino feliz, puede guardar una instantánea del agregado en la memoria; solo necesita comunicarse con una tienda externa cuando no hay una copia local disponible.
Además, no necesita estar completamente atrapado si tiene acceso al libro de registro.
Entonces, el enfoque habitual ( si se usa un repositorio de instantáneas) es mantenerlo de forma asíncrona . De esa manera, si necesita recuperarse, puede hacerlo sin volver a cargar y reproducir todo el historial del agregado.
Hay muchos casos en los que esta complejidad no es de interés, porque los agregados de grano fino con vidas de alcance generalmente no recopilan suficientes eventos para que los beneficios excedan los costos de mantener un caché de instantáneas.
Pero cuando es la herramienta adecuada para el problema, cargar una representación obsoleta del agregado en el modelo de escritura y luego actualizarlo con eventos adicionales es algo perfectamente razonable.
fuente
Como no especifica cuál sería el propósito de la base de datos de "escritura", supondré aquí que lo que quiere decir es esto: al registrar una nueva actualización en un agregado, en lugar de reconstruir el agregado desde el almacén de eventos, usted levantarlo de la base de datos "escribir", validar el cambio y emitir un evento.
Si esto es lo que quiere decir, entonces esta estrategia creará una condición para la inconsistencia: si una nueva actualización ocurre antes de que la última tuviera la oportunidad de ingresar a la base de datos de "escritura", la nueva actualización terminará validada contra datos obsoletos, potencialmente emitiendo un evento "imposible" (es decir, "no permitido") y corrompiendo el estado del sistema.
Por ejemplo, considere un ejemplo permanente de reservar asientos en un teatro. Para evitar la doble reserva, debe asegurarse de que el asiento que se está reservando no esté ocupado, esto es lo que llama "validación". Para hacer eso, almacena una lista de asientos ya reservados en la base de datos "escribir". Luego, cuando llega una solicitud de reserva, verifica si el asiento solicitado está en la lista y, si no, emite un evento "reservado", de lo contrario responde con un mensaje de error. Luego ejecuta un proceso de proyección, donde escucha los eventos "reservados" y agrega los asientos reservados a la lista en la base de datos "escribir".
Normalmente, el sistema funcionaría así:
Sin embargo, ¿qué pasa si las solicitudes llegan demasiado rápido y el paso 5 ocurre antes del paso 4?
Ahora tiene dos eventos para reservar el mismo asiento. El estado del sistema está dañado.
Para evitar que esto suceda, nunca debe validar las actualizaciones contra una proyección. Para validar una actualización, reconstruye el agregado desde el almacén de eventos y luego valida la actualización contra él. Después de eso, emite un evento, pero utiliza la protección de marca de tiempo para asegurarse de que no se hayan emitido nuevos eventos desde la última vez que leyó en la tienda. Si esto falla, solo vuelve a intentarlo.
La reconstrucción de agregados de la tienda de eventos puede conllevar una penalización de rendimiento. Para mitigar esto, puede almacenar instantáneas agregadas directamente en la secuencia del evento, etiquetadas con el ID del evento desde el que se creó la instantánea. De esta manera, puede reconstruir el agregado cargando la instantánea más reciente y reproduciendo solo los eventos que vinieron después, en lugar de reproducir siempre todo el flujo de eventos desde el principio de los tiempos.
fuente
La razón principal es el rendimiento. Puede almacenar una instantánea para cada confirmación (commit = los eventos generados por un solo comando, generalmente solo un evento), pero esto es costoso. A lo largo de la instantánea, debe almacenar también la confirmación, de lo contrario no sería el abastecimiento de eventos. Y todo esto debe hacerse atómicamente, todo o nada. Su pregunta es válida solo si se utilizan bases de datos / tablas / colecciones separadas (de lo contrario, sería exactamente el abastecimiento de eventos), por lo que se ve obligado a usar transacciones para garantizar la coherencia. Las transacciones no son escalables. Una secuencia de eventos de solo agregado (la tienda de eventos) es la madre de la escalabilidad.
La segunda razón es la encapsulación agregada. Necesitas protegerlo. Esto significa que el Agregado debería ser libre de cambiar su representación interna en cualquier momento. Si lo almacena y depende en gran medida de él, tendrá dificultades con el control de versiones, lo que seguramente sucederá. En la situación en la que utiliza la instantánea solo como una optimización, cuando el esquema cambia, simplemente ignora esas instantáneas ( ¿ simplemente ? Realmente no lo creo; buena suerte para determinar que el esquema de Aggregate cambia, incluidas todas las entidades anidadas y los objetos de valor, en un de manera eficiente y gestionando eso).
fuente