Então, eu tenho flertado com o Event Sourcing e o CQRS há um tempo, embora nunca tenha tido a oportunidade de aplicar os padrões em um projeto real.
Entendo os benefícios de separar suas preocupações de leitura e gravação e aprecio como o Event Sourcing facilita o projeto de alterações de estado nos bancos de dados "Modelo de Leitura" que são diferentes do seu Armazenamento de Eventos.
O que não sou muito claro é por que você reidrataria seus agregados da própria loja de eventos.
Se projetar alterações nos bancos de dados "ler" é tão fácil, por que nem sempre projetar alterações em um banco de dados "gravar" cujo esquema corresponde perfeitamente ao seu modelo de domínio? Este seria efetivamente um banco de dados de instantâneos.
Eu imagino que isso deve ser bastante comum em aplicativos ES + CQRS em estado selvagem.
Se for esse o caso, o Armazenamento de Eventos é útil apenas ao reconstruir seu banco de dados "gravação" como resultado de alterações de esquema? Ou estou perdendo algo maior?
fonte
Respostas:
Porque os "eventos" são o livro de registro.
Sim; às vezes é uma otimização de desempenho útil usar uma cópia em cache do estado agregado, em vez de regenerar esse estado do zero toda vez. Lembre-se: a primeira regra de otimização de desempenho é "Não". Ele adiciona complexidade extra à solução, e você prefere evitar isso até ter uma motivação comercial atraente.
Está faltando algo maior.
O primeiro ponto é que, se você está considerando uma solução de origem de evento, é porque espera que haja valor em preservar o histórico do que aconteceu, ou seja, que você deseja fazer alterações não destrutivas .
É por isso que estamos escrevendo para a loja do evento.
Em particular, isso significa que todas as alterações precisam ser gravadas no armazenamento de eventos.
Os escritores concorrentes podem potencialmente destruir as gravações um do outro ou levar o sistema a um estado não intencional, se não estiverem cientes das edições um do outro. Portanto, a abordagem usual quando você precisa de consistência é direcionar suas gravações para uma posição específica no diário (análoga a uma PUT condicional em uma API HTTP). Uma gravação com falha informa ao escritor que seu entendimento atual do diário está fora de sincronia e que eles devem se recuperar.
Retornar a uma boa posição conhecida e reproduzir quaisquer eventos adicionais, pois esse ponto é uma estratégia de recuperação comum. Essa boa posição conhecida pode ser uma cópia do que está no cache local ou uma representação no seu armazenamento de instantâneos.
No caminho feliz, você pode manter um instantâneo do agregado na memória; você só precisa acessar um armazenamento externo quando não houver uma cópia local disponível.
Além disso, você não precisa ser completamente informado, se tiver acesso ao livro de registro.
Portanto, a abordagem usual ( se estiver usando um repositório de instantâneos) é mantê-lo de forma assíncrona . Dessa forma, se você precisar se recuperar, poderá fazê-lo sem recarregar e reproduzir o histórico inteiro do agregado.
Há muitos casos em que essa complexidade não é interessante, porque agregados refinados com vida útil no escopo geralmente não coletam eventos suficientes para que os benefícios excedam os custos de manutenção de um cache de captura instantânea.
Mas quando é a ferramenta certa para o problema, carregar uma representação obsoleta do agregado no modelo de gravação e atualizá-lo com eventos adicionais é uma coisa perfeitamente razoável a se fazer.
fonte
Como você não especifica qual seria o objetivo do banco de dados "write", assumirei aqui que o que você quer dizer é o seguinte: ao registrar uma nova atualização em um agregado, em vez de reconstruir o agregado do armazenamento de eventos, você levante-o do banco de dados "write", valide a alteração e emita um evento.
Se é isso que você quer dizer, essa estratégia criará uma condição de inconsistência: se uma nova atualização ocorrer antes que a última tenha a chance de entrar no banco de dados "write", a nova atualização será validada contra dados desatualizados, potencialmente emitindo um evento "impossível" (isto é, "não permitido") e corrompendo o estado do sistema.
Por exemplo, considere um exemplo permanente de reservar assentos em um teatro. Para evitar a reserva dupla, é necessário garantir que o lugar que está sendo reservado ainda não esteja ocupado - é o que você chama de "validação". Para fazer isso, você armazena uma lista de assentos já reservados no banco de dados "write". Quando uma solicitação de reserva é recebida, você verifica se o assento solicitado está na lista e, se não, emite um evento "reservado"; caso contrário, responda com uma mensagem de erro. Em seguida, você executa um processo de projeção, no qual ouve os eventos "reservados" e adiciona os assentos reservados à lista no banco de dados "write".
Normalmente, o sistema funcionaria assim:
No entanto, e se as solicitações forem recebidas muito rapidamente e a etapa 5 acontecer antes da etapa 4?
Agora você tem dois eventos para reservar o mesmo lugar. O estado do sistema está corrompido.
Para impedir que isso aconteça, você nunca deve validar atualizações contra uma projeção. Para validar uma atualização, recrie o agregado do armazenamento de eventos e valide a atualização. Depois disso, você emite um evento, mas use a proteção de carimbo de data e hora para garantir que nenhum novo evento tenha sido emitido desde a última leitura da loja. Se isso falhar, basta tentar novamente.
A reconstrução de agregados a partir do armazenamento de eventos pode acarretar uma penalidade no desempenho. Para atenuar isso, é possível armazenar capturas instantâneas agregadas diretamente no fluxo de eventos, marcado com o ID do evento a partir do qual a captura instantânea foi criada. Dessa forma, você pode reconstruir o agregado carregando a captura instantânea mais recente e reproduzindo apenas os eventos que vieram depois dela, em vez de sempre reproduzir a sequência inteira de eventos desde o início.
fonte
O principal motivo é o desempenho. Você pode armazenar um instantâneo para cada confirmação (commit = os eventos que são gerados por um único comando, geralmente apenas um evento), mas isso é caro. Ao longo da captura instantânea, é necessário armazenar também a confirmação, caso contrário, não seria a origem do evento. E tudo isso deve ser feito atomicamente, tudo ou nada. Sua pergunta é válida apenas se bancos de dados / tabelas / coleções separados forem usados (caso contrário, seria exatamente a origem do evento), portanto você será forçado a usar transações para garantir consistência. As transações não são escaláveis. Um fluxo de eventos somente para acréscimo (o armazenamento de eventos) é a mãe da escalabilidade.
O segundo motivo é o encapsulamento agregado. Você precisa protegê-lo. Isso significa que o Agregado deve estar livre para alterar sua representação interna a qualquer momento. Se você armazená-lo e depender muito dele, terá muita dificuldade com o controle de versão, o que acontecerá com certeza. Na situação em que você usa o instantâneo apenas como otimização, quando o esquema muda, você simplesmente ignora esses instantâneos ( simplesmente ? Acho que não; boa sorte ao determinar que o esquema do Agregado é alterado - incluindo todas as entidades aninhadas e objetos de valor - em um maneira eficiente e de gerenciar isso).
fonte