Tempo de leitura: 7 minutos
Se você não sabe o que é CQRS, recomendo a leitura de uma pequena revisão que escrevi há bem pouco tempo. O mesmo vale para Event Sourcing
Sobre RavenDB
Antes de começar a falar sobre CQRS e Event Sourcing, vamos introduzir alguns conceitos fundamentais sobre RavenDB: Document, Index e Transformer.
Document
São objetos JSON, como o apresentado abaixo, representando o que estamos persistindo no banco de dados.
{ "Name": { "GivenName": "John", "Surname": "Doe" }, "Salary": 256, "HomeAddress": { "$type": "Payroll.Domain.Model.BrazilianAddress, Payroll.Domain", "StreetName": "Main avenue", "Number": 42, "AdditionalInfo": null, "Neighborhood": "Good Ville", "City": "MyCity", "State": "XX", "PostalCode": "91234-123", "Country": "Brazil" } }
Esses objetos não precisam seguir nenhuma especificação de esquema e geralmente estão agrupados em "coleções".
Em um primeiro momento, podemos pensar em documentos como registros e coleções como tabelas em um banco de dados relacional. Entretanto, no RavenDB cada documento poderá adotar a estrutura que for mais apropriada.
RavenDB oferece suporte a transações para manutenção de documentos. Por isso, é considerado um banco que suporta ACID.
Index
São funções que são executadas no lado do servidor e que determinam que campos (e quais valores) podem ser utilizados em buscas. Essas funções geralmente criam estruturas semelhantes a materialized views que aceleram a obtenção de resultados.
Todo o processo de indexação acontece em background e é disparado sempre que um dado é adicionado ou modificado. Essa abordagem, permite que o servidor responda rapidamente mesmo quando grandes porções de dados foram modificadas e evita buscas mais pesadas. Ema compartida, não há garantias de que os dados retornados/armazenados nos índices estejam atualizados.
Índices no RavenDB são eventualmente consistentes.
Transformações
São funções que são executadas no lado do servidor, capazes de fazer projeções, aceitando parâmetros, refinando resultados e mais.
Diferente dos índices, transformações não criam estruturas adicionais de dados no servidor sendo utilizadas apenas durante consultas em que são invocadas.
RavenDB e CQRS
Por ser um banco de documentos, livre de esquema, RavenDB é uma excelente opção para banco de dados Query Stack. Mas, não precisamos parar por aí.
Vejamos:
Como a figura sugere, podemos pensar em índices e transformações como fontes excelentes de dados para a Query Stack. De forma análoga, os documentos, pela sua natureza ACID, são perfeitas para a Command Stack.
Persistência adequadas ao domínio com RavenDB
Seguindo DDD, buscamos adotar linguagem ubíqua nos serviços de domínio. Não é diferente com o repositório.
public interface IEmployeeRepository { bool IsRegistered(EmployeeId id); Employee Load(EmployeeId id); void CreateEmployee( EmployeeId id, FullName name, decimal initialSalary); void RaiseSalary(EmployeeId id, decimal amount); void UpdateHomeAddress(EmployeeId id, Address homeAddress); }
Salvando e recuperando documentos
A implementação em RavenDB para essa interface não oferece grandes dificuldades.
public class EmployeeRepository : IEmployeeRepository, IDisposable { private readonly IDocumentStore _store; private JsonSerializer _serializer; public EmployeeRepository() { /* implementation omitted */ } public bool IsRegistered(EmployeeId id) { return _store.DatabaseCommands.Head($"employees/{id}") != null; } public Employee Load(EmployeeId id) { Employee result; using (var session = _store.OpenSession()) { result = session.Load<Employee>($"employees/{id}"); } return result; } public void CreateEmployee(EmployeeId id, FullName name, decimal initialSalary) { using (var session = _store.OpenSession()) { var employee = new Employee(id, name, Address.NotInformed, initialSalary); session.Store(employee); session.SaveChanges(); } } public void RaiseSalary(EmployeeId id, decimal amount) { /* implementation omitted * } public void UpdateHomeAddress(EmployeeId id, Address homeAddress) { /* implementation omitted * } public void Dispose() { _store.Dispose(); } }
Salvar documentos é trivial. Você pode encontrar mais informações sobre como isso ocorre nesse post.
Suportando polimorfismo
Diferentes países tem diferentes formatos de endereço. Meu domínio reconhece esse fato e possui diferentes implementações, uma para cada localidade..
public abstract class Address { public abstract string Country { get; } } public sealed class BrazilianAddress : Address { public string StreetName { get; private set; } public int Number { get; private set; } public string AdditionalInfo { get; private set; } public string Neighborhood { get; private set; } public string City { get; private set; } public string State { get; private set; } public string PostalCode { get; private set; } public override string Country => "Brazil"; public override bool Equals(object obj) { /* implementation omitted /* } public override int GetHashCode() { /* implementation omitted /* } public static class Factory { /* implementation omitted /* } }
RavenDB, com seu esquema livre, não cria dificuldades para que eu faça a persistência.
public void UpdateHomeAddress(EmployeeId id, Address homeAddress) { _store.DatabaseCommands.Patch($"employees/{id}", new[] { new PatchRequest { Type = PatchCommandType.Set, Name = "HomeAddress", Value = RavenJObject.FromObject(homeAddress, _serializer) } }); }
O serializador adiciona meta-atributos que garantem que, na carga do objeto, o tipo adequado será utilizado.
Executando atualizações com scripts
Outro ponto a destacar no repositório é o método que trata do aumento do salário:
public void RaiseSalary(EmployeeId id, decimal amount) { _store.DatabaseCommands.Patch($"employees/{id}", new ScriptedPatchRequest { Script = $"this.Salary += {amount.ToInvariantString()};" }); }
Não há necessidade de recuperar informações do banco. O Script enviado é um Javascript.
RavenDB e Event Sourcing
A ideia fundamental de ES é armazenar os diversos eventos gerados pelo domínio como fonte primária de dados.
Essa abordagem permite recuperar o estado de qualquer entidade/agregado simplesmente "refazendo" as modificações registradas nos eventos. O grande ganho é que podemos saber o estado de qualquer entidade/agregado ao longo do tempo.
A implementação de ES com RavenDB é bem simples. Caso o sistema já emita eventos basta enviar os objetos correspondentes para armazenamento como documentos.
public class EmployeeEventStore : IMessageHandler<EmployeeRegisteredEvent>, IMessageHandler<EmployeeHomeAddressUpdatedEvent>, IMessageHandler<EmployeeSalaryRaisedEvent>, IDisposable { private readonly DocumentStore _store; public EmployeeEventStore() { _store = new DocumentStore { Url = "http://localhost:8080/", // server URL DefaultDatabase = "RegularDb" }; _store.Conventions.CustomizeJsonSerializer = serializer => serializer.Converters.Add(new EmployeeIdJsonConverter()); _store.Initialize(); _store.Conventions.FindTypeTagName = t => "EmployeeEvents"; } public void HandleInternal(Message message) { using (var session = _store.OpenSession()) { session.Store(message); session.SaveChanges(); } } public void Handle(EmployeeRegisteredEvent message) { HandleInternal(message); } public void Handle(EmployeeHomeAddressUpdatedEvent message) { HandleInternal(message); } public void Handle(EmployeeSalaryRaisedEvent message) { HandleInternal(message); } public void Dispose() { _store.Dispose(); } public class EmployeeIdJsonConverter : JsonConverter { /* implementation omitted */ } }
O que fizemos aqui foi manifestar (para posterior registro no BUS) que temos interesse em todos os eventos relacionados a Employee. Além disso, para fins de organização, configurei para que todos os documentos fossem adicionados em uma mesma coleção.
Usando Map/Reduce para projetar uma ViewModel a partir dos eventos (número de eventos por funcionário)
Armazenar eventos, na Command Stack abre possibilidades e desafios técnicos interessantes. Como produzir ViewModels para a Query Stack? Se estivermos utilizando RavenDB podemos frequentemente recorrer aos índices.
Comecemos com um exemplo bem simples:
public class EmployeeEventsSummaryResult { public EmployeeId EmployeeId { get; set; } public int NumberOfEvents { get; set; } } class EmployeeEventsSummaryIndex : AbstractIndexCreationTask<EmployeeEvent, EmployeeEventsSummaryResult> { public override string IndexName => "EmployeeEvents/Summary"; public EmployeeEventsSummaryIndex() { Map = (events) => from e in events select new EmployeeEventsSummaryResult { EmployeeId = e.EmployeeId, NumberOfEvents = 1 }; Reduce = (inputs) => from input in inputs group input by input.EmployeeId into g select new EmployeeEventsSummaryResult { EmployeeId = g.Key, NumberOfEvents = g.Sum(x => x.NumberOfEvents) }; } }
O que esse índice responde é o total de eventos associados a cada entidade Employee. Ele faz isso através da aplicação da técnica Map Reduce.
Durante o Map todos os eventos registrados são convertidos (mapeados) para uma representação comum.
Depois, essas representações são agrupadas atendendo a um critério.
Por fim, ocorre a redução:
A beleza é que pela natureza do algoritmo, a atualização do índice ocorre muito rapidamente (um map, um reduce).
Usando Map/Reduce para projetar uma ViewModel a partir dos eventos (Os mais bem pagos)
Usando uma ideia análoga a anterior, vamos construir um outro índice.
public class EmployeeSalary { public EmployeeId EmployeeId { get; set; } public string FullName { get; set; } public decimal Salary { get; set; } } public class EmployeeEventsSalaryPerEmployee : AbstractMultiMapIndexCreationTask<EmployeeSalary> { public override string IndexName => "EmployeeEvents/SalaryPerEmployee"; public EmployeeEventsSalaryPerEmployee() { AddMap<EmployeeSalaryRaisedEvent>(events => from e in events where e.MessageType == "EmployeeSalaryRaisedEvent" select new { e.EmployeeId, FullName = "", Salary = e.Amount }); AddMap<EmployeeRegisteredEvent>(events => from e in events where e.MessageType == "EmployeeRegisteredEvent" select new { e.EmployeeId, FullName = e.Name.GivenName + " " + e.Name.Surname, Salary = e.InitialSalary }); Reduce = inputs => from input in inputs group input by input.EmployeeId into g select new EmployeeSalary() { EmployeeId = g.Key, FullName = g.Aggregate("", (a, b) => b.FullName != "" ? b.FullName : a), Salary = g.Sum(x => x.Salary) }; } }
Este índice, diferente do anterior, utiliza duas fontes par mapping. Não há problema com isso, desde que a saída seja comum.
Perceba que durante o map usamos as propriedades InitialSalary para o evento de funcionário adicionado e Amount para o evento de aumento.
Atendendo a filosofia de entregar um modelo "pronto para o uso", estou gerando um FullName concatenando as partes no evento de funcionário adicionado. Deixo o nome em branco para o evento de aumento pois esse registro não possui a informação.
O Reduce é quase auto explicativo. Basicamente, estou somando os salários e pegando o nome.
O consumo dessa informação é muito fácil também.
public IEnumerable<EmployeeSalary> TopSalaries() { IEnumerable<EmployeeSalary> results; using (var session = _store.OpenSession()) { results = session .Advanced .DocumentQuery<EmployeeSalary, EmployeeEventsSalaryPerEmployee>() .OrderByDescending(es => es.Salary) .ToList(); } return results; }
Concluindo
Phew!
Nesse post vimos como RavenDB possui fit natural com CQRS e, até mesmo, com Event Sourcing.
Para começar, temos a facilidade da estrutura de documentos com esquema livre. Ela é ideal para armazenamento de objetos prontos para consumir (ideais para Query Stack) e permite fácil adoção de abordagem polimórficas. Além disso, por fornecer transações, é sólida o suficiente para ser utilizada como base de documentos.
A característica de permitir esquema livre também facilita muito a criação de um repositório de eventos, caso desejemos trabalhar com Event Sourcing. Eventos são fáceis de registrar e consultar.
Por fim, o suporte a índices permite a criação de mecanismos avançados de projeção.
Era isso.
Você pode saber mais sobre RavenDB acessando o site oficial