CQRS, interfaces, queries, ORMs….cuidado!

Olá, pessoal

Neste artigo, veremos um pouco de CQRS, alguns erros cometidos e lições aprendidas em minhas primeiras incursões a este padrão.

Espero que sirva de alerta para os marinheiros de primeira viagem quanto a algumas escolhas de design e ferramentas.

Vamos lá!

CQRS – UMA BREVE INTRODUÇÃO

CQRS (Command/Query Responsibility Segregation) é um padrão cuja ideia basicamente consiste em quebrar um objeto – composto de comandos (commands) e consultas (queries) – em dois objetos, um somente com comandos e outro somente com consultas.

(Nota: Os conceitos de “comando” e “consulta” são os mesmos já definidos pelo CQS (Command/Query Separation), uma ideia proposta por Bertrand Meyer, em relação aos métodos de um objeto.)

Essa simples ideia do CQRS abre uma infinidade de oportunidades em termos de arquitetura.

A primeira e principal delas é aplicar o padrão para o modelo todo, quebrando-o em um modelo somente para comandos e outro somente para consultas. Esta separação possibilita uma série de outras vantagens, como o uso de tecnologias diferentes nos dois modelos, esquemas e tipos diferentes de banco de dados.

Abaixo segue um exemplo de CQRS, em sua forma mais completa, com 2 bancos separados:

cqrs-canonico(Imagem retirada do artigo “Types of CQRS”)

No final deste artigo, forneço uma série de referências para outros artigos mais completos sobre o padrão, incluindo diversos outros padrões e práticas que podem ser usadas em conjunto com o CQRS.

IMPLEMENTANDO O “COMMAND MODEL”

Neste lado, temos o domain model que lidará com todas as operações de negócio, causando alguma alteração de estado em nosso sistema.

Aqui, uma prática comum em se tratando de um domínio complexo é implementarmos um modelo orientado a objetos e utilizarmos algum ORM para facilitar com a persistência de dados.

Obviamente, necessitamos neste lado de algumas consultas para poder recuperar os objetos (“hidratar” no linguajar dos ORMs) antes de chamar seus métodos. Isso se dá, normalmente, em um modelo rico, por meio do Repository Pattern. No entanto, como segregamos nosso modelo, os repositórios expõem consultas bem simples já que não têm a responsabilidade de retornar dados complexos para telas, relatórios ou outros sistemas.

No final do artigo, há um link para outro artigo meu que mostra uma possível forma de implementar o lado “command” da arquitetura.

IMPLEMENTANDO O “QUERY MODEL”

Neste lado, temos um modelo que lida com o resultados das consultas, ou seja, seus “objetos” são meras sacolas de dados que, em geral, espelham o modelo de dados.

Aqui também possuímos objetos especializados em consultas, que chamamos de “query objects” e que são bem mais “tunados” que os repositórios do domain model e retornam as sacolas de dados antes mencionadas.

Daqui em diante, mostrarei algumas escolhas adotadas na implementação das consultas e as limitações e problemas causados. Portanto, NÃO trate como uma solução “correta” para o lado “query”.

(Obs.: alguns detalhes e variações utilizadas ao longo do caminho foram omitidos para deixar o artigo mais enxuto e objetivo.)

Começamos com uma interface base para as consultas, localizada na camada de aplicação:

public interface IQueryObject<TEntity, TResult>
{
    PagedResult<TResult> GetPagedResultsBySpec(int currentPage, int numberOfPages, ISpecification<TEntity> spec, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderByExpression = null);
    IEnumerable<TResult> GetAllBySpec(ISpecification<TEntity> spec, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderByExpression = null);
}

(ATENÇÃO: a interface acima é própria da solução, ou seja, ela NÃO faz parte do .Net nem de qualquer lib de terceiro.)

A princípio, todas as consultas implementavam a interface acima, o que nos levava a classes como essas:

public class ConsultaDeClientes :
             IQueryObject<Cliente,ClienteDto> { /*...*/ }

public class ConsultaDePedidos :
             IQueryObject<Pedido, PedidoDto> { /*...*/ }

Interface confusa e limitada

Um dos problemas com essa solução foi a incoerência: afinal, IQueryObject<> deveria permitir consultas flexíveis ou não?

Explico. De acordo com os métodos da interface, eu consigo filtrar os resultados usando uma specification. Portanto, poderia chamar uma consulta de clientes de várias formas:

// _query implementa IQueryObject<Cliente,ClienteDto>
var dto = _query.GetAllBySpec(new ClientesInativos() /*...*/ );
// ou...
var dto = _query.GetAllBySpec(new ClientesMaioresDeIdade() /*...*/);

Vejam que, independente do filtro usado (clientes inativos ou clientes maiores de idade), o DTO retornado é o mesmo: ClienteDto. Mas será que são os mesmos dados que eu preciso nessas consultas? É bem provável que não!

Alguém poderia pensar: “Basta criar outra consulta que implemente IQueryObject<Cliente, AlgumOutroDto> com os dados que eu preciso”. Percebam que esta não seria a melhor solução, pois ainda teríamos uma consulta flexível no filtro porém inflexível no retorno. Quais specs usar com qual consulta/DTO?


Vamos a outro problema encontrado com o uso da interface IQueryObject. Esta interface indica um “de->para” de 1 entidade de domínio para 1 DTO. Sendo assim, ela deixa “vazar” detalhes de implementação: a origem dos dados para o DTO vem de objetos de domínio, assim como o filtro utilizado!

E se quiséssemos trocar a implementação de uma consulta para que buscasse os dados direto do banco de dados e preenchesse o DTO? Teríamos que fazer com que a nova consulta NÃO implementasse IQueryObject e seríamos obrigados a alterar o código-cliente para usar a nova interface (por causa de um detalhe de implementação: “como” a consulta é feita). Matamos uma das vantagens da interface!

O motivo de IQueryObject ter sido criada da forma que foi – dependendo de uma entidade do domínio – deve-se ao fato de as implementações fazerem uso do ORM (NHibernate) para preencher os dados das entidades. Este problema será tratado no tópico a seguir…

Dependência do “command model” e uso de ORM

Como dito anteriormente, todas as consultas do “query model” fazem uso de um ORM (NHibernate).

Isso pode ser um grande problema em aplicações onde o volume de consultas seja muito grande e escalabilidade seja um requisito fundamental. Em outras palavras, sua consulta pode não rodar em tempo satisfatório, gerando queixas dos clientes, perda de visitas em um site, por exemplo, e consequentemente, perda de receita!

Vimos também, pela especificação da interface IQueryObject, que não usamos diretamente o mapeamento (via ORM) para os DTOs. Continuamos com o mapeamento para entidades de domínio, que por sua vez, são mapeadas no código para os DTOs (reaproveitando as mesmas classes de mapeamento usadas para o “command model”).

Isso dá a oportunidade de, na implementação da consulta, chamarmos um “cliente.Pedidos”, configurado com lazy-loading, gerando mais problemas de lentidão.

Além disso, essa dependência do “query model” para o “command model” (onde estão as entidades) pode se tornar um empecilho no futuro – ou no presente – caso haja a necessidade de rodar os dois modelos em processos separados (dependendo da quantidade de consultas, seria um grande trabalho quebrar essa dependência).

CONCLUINDO…

Neste artigo, compartilhei com vocês alguns dos problemas encontrados nas primeiras tentativas de utilizar CQRS (que costumo chamar de “CQRS de pobre”, já que não chegamos a uma solução com os modelos e esquemas de banco totalmente desacoplados).

Em resumo, ficam as seguintes dicas:

  • Implemente algumas de suas consultas primeiro e abstraia depois. Não tente criar, de início, uma interface supondo uma série de coisas e não a utilize se você notar problemas de design com ela.
  • Você não é obrigado a usar exatamente as mesmas ideias, práticas e tecnologias nos dois modelos. Uma das vantagens da separação entre “queries” e “commands” é justamente te dar flexibilidade para usar a melhor solução para cada um dos lados.
  • Em especial, mesmo que não esteja utilizando CQRS, evite utilizar ORMs em consultas complexas, com grande volume de dados como resultado e milhares de acessos.

Existem inúmeras formas de implementar ambos os lados do CQRS. Em um próximo artigo, pretendo mostrar uma forma de implementar o lado “query”, evitando as limitações mostradas neste artigo.

Espero que tenham gostado. Dúvidas? Críticas? Já trabalham com CQRS? Estão começando? Como estão implementando? Comentem aí!

Abaixo os links prometidos:
CQRS:
http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/
http://martinfowler.com/bliki/CQRS.html
http://enterprisecraftsmanship.com/2015/04/20/types-of-cqrs/
http://elemarjr.com/pt/2016/04/11/uma-breve-revisao-de-cqrs
https://robsoncastilho.com.br/2015/11/26/implementado-servicos-de-aplicacao-transacionais/ (uma forma de implementar serviços de aplicação no lado “command”)
ORMs nos dois modelos (queries e commands):
http://enterprisecraftsmanship.com/2015/11/30/do-you-need-an-orm/

Anúncios

7 comentários em “CQRS, interfaces, queries, ORMs….cuidado!

  1. Acredito que o maior problema da interface IQueryObject é o “De-Para” pois dessa forma há perde performasse, para solucionar esse problema poderia ser usado ADO.NET com uma query sql especifica ou até mesmo um micro ORM como o DAPPER.

  2. Uma dúvida o NHibernate é mais rápido que o Entity Framework?
    Por que você você usou o NHibernate?

    1. Olá, Alexandre

      Não sei te dizer hoje como está o comparativo entre a performance dos dois (você deve achar na web alguns benchmarks sobre isso). Sei que nos primórdios, o NH era sim bem melhor que o EF, que gerava queries ruins.

      Escolhi o NH porque ele era o mais usado e consolidado no mundo .Net. Um dos principais motivos, do ponto de vista técnico, é que interfere muito pouco na modelagem de seus objetos de domínio, ie, eles são o mais ‘persistence ignorant’ possíveis. Além disso, ele funciona, há muito tempo, muito bem com diversos tipos/estrutura de dados, como enums, por exemplo, e diferentes bancos de dados.

      Hoje em dia, o EF está bem mais maduro e tem um ciclo de releases muito mais rápido do que o NH (que ficou “um século” parado na 3.x), portanto seria uma opção a ser avaliada em projetos futuros.

      []s

  3. Robson,

    No diagrama do CQRS acima existe um sincronizador entre a base relacional e nosql.
    Este mecanismo seria um service bus?
    Se sim existem alguns bom para plataforna .NET grátis?

    []’s

    1. Sim. Em geral, essa sincronização é assincrona. Existem buses que abstraem essa tarefa , como o NServiceBus e o MassTransit, mas não cheguei a usá-los com propriedade ainda.
      []s

  4. Robson, você teria algum exemplo prático pra ver o uso disso. Pois ainda não consegui enchergar essa visão. Estou querendo fazer testes.

    Desde já grato.

    1. Oi, Tiago

      Não tenho nenhum exemplo no github :/ e não existe uma forma única de implementar, mas a ideia é bem simples.

      Mostrando um exemplo bem básico mas que dá uma ideia no cast que fizemos com o Elemar Jr sobre CQRS. Se você não assistiu, dá uma olhada aqui:
      https://www.youtube.com/watch?v=3kbJeDYBx-w (não sei se está no github de alguém, preciso confirmar).
      (No final do artigo, também coloquei uns links que ajudam)

      Qualquer dúvida, posta aí.
      []s

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s