Novembro de 2016

Volume 31 - Número 11

Pontos de Dados - Modelos de Dados CQRS e EF

De Julie Lerman

Julie LermanO CQRS (Command Query Responsibility Segregation) é um padrão que, essencialmente, fornece diretrizes sobre a separação da responsabilidade de ler os dados e causar uma alteração no estado de um sistema (por exemplo, o envio de uma mensagem de confirmação ou a gravação em um banco de dados) e o design adequado de objetos e da arquitetura. Inicialmente, ele foi desenvolvido para ajudar em sistemas altamente transacionais, como o bancário. Greg Young desenvolveu o CQRS a partir da estratégia CQS (command-query separation), de Bertrand Meyer, de acordo com Martin Fowler, “será extremamente útil se você puder separar claramente os métodos que alteram o estado daqueles que não fazem isso” (bit.ly/2cuoVeX). O que o CQRS adiciona é a ideia da criação de modelos totalmente separados para comandos e consultas.

Com frequência, o CQRS tem sido colocado em buckets incorretamente como um tipo particular de arquitetura ou como parte do Design Orientado a Domínio ou como um sistema de mensagens ou de eventos. Em uma postagem de blog de 2010, “CQRS, Task-Based UIs, Event Sourcing, Agh!” (CQRS, Interfaces do Usuário Baseadas em Tarefas, Origem dos Eventos, Agh!) (bit.ly/1fZwJ0L), Young explica que o CQRS não é nada disso, mas apenas um padrão que pode ajudar na tomada de decisões de arquitetura. Na verdade, o CQRS se trata de “ter dois objetos onde antes havia somente um”. Ele não é específico de modelos de dados ou de limites de serviço, embora certamente possa ser aplicado a essas partes do seu software. Na verdade, ele declara que “o maior benefício possível é que ele reconhece que suas propriedades de arquitetura são diferentes ao lidar com comandos e consultas.

Ao definir modelos de dados (em grande parte com o EF [Entity Framework]), me tornei um fã do aproveitamento desse padrão, em determinados cenários. Como sempre, minhas ideias são apenas diretrizes e não regras e, assim como optei por aplicar CQRS de uma maneira que me ajude a obter a minha arquitetura, espero que você as use e as modele para que se ajustem às suas próprias necessidades.

Benefícios da Manipulação de Relacionamentos com o EF

O Entity Framework facilita o trabalho com relacionamentos no tempo de design. Durante as consultas, isso é um benefício enorme. Os relacionamentos entre as entidades permitem que você navegue nesses relacionamentos ao expressar as consultas. A recuperação de dados relacionados do banco de dados é fácil e eficiente. Você pode optar pelo carregamento adiantado com o método Include ou com projeções, pelo carregamento lento após o fato ou pelo carregamento explícito após o fato. Esses recursos não mudaram muito desde a versão original do EF, nem desde que escrevi sobre eles em junho de 2011, “Desmistificando Estratégias do Entity Framework: Carregando Dados Relacionados” (msdn.com/magazine/hh205756).

O exemplo canônico no modelo da Figura 1 facilita as consultas para exibir os detalhes do pedido de um cliente, os itens de linha e os nomes de produto em uma página. Você pode escrever uma consulta eficiente como esta:

var customersWithOrders = context.Customers
  .Include(c => c.Orders.Select(
  o => o.LineItems.Select(p => p.Product)))
  .ToList();

Modelo de Dados do Entity Framework com Relacionamentos Altamente Acoplados
Figura 1 Modelo de Dados do Entity Framework com Relacionamentos Altamente Acoplados

O EF transformará isso em SQL que recuperará todos os dados relevantes em um comando de banco de dados. Então, a partir dos resultados, o EF materializará os gráficos completos de clientes, os pedidos deles, os itens da linha dos pedidos e até os detalhes do produto para cada item de linha.

Ele facilita mesmo o preenchimento de uma página como a janela do WPF (Windows Presentation Foundation) na Figura 2. Posso fazer isso em uma única linha de código:

customerViewSource.Source = customersWithOrders

Controles de Dados Associados a um Único Gráfico de Objeto
Figura 2 Controles de Dados Associados a um Único Gráfico de Objeto

Veja outro benefício que os desenvolvedores amam: Ao criar gráficos, o EF organizará o entra e sai do banco de dados insira o pai, retorne o valor da nova chave primária e, em seguida, aplique isso como a chave estrangeira para os filhos antes de criar e de executar os comandos insert.

É quase mágico. Mas a mágica tem suas desvantagens e, no caso dos modelos de dados do EF, a mágica de ter relacionamentos associados pode resultar em efeitos colaterais quando for a hora de executar atualizações e, às vezes, até com consultas. Um efeito colateral notável pode acontecer quando você anexa dados de referência a um novo registro usando uma propriedade de navegação e, em seguida, chama SaveChanges. Como exemplo, você poderia criar um novo item de linha e definir sua propriedade Product para uma instância de um produto existente que veio do banco de dados. Em um aplicativo conectado, como o aplicativo do WPF, onde o EF pode controlar todas as alterações em seus objetos, o EF pensará que o produto era pré-existente. No entanto, em cenários desconectados onde o EF começa a controlar os objetos somente depois que as alterações tiverem sido feitas, o EF assumirá que o produto, como o item de linha, é novo e irá inseri-lo no banco de dados novamente. Mas claro, há soluções alternativas para esses problemas. No caso desse problema, sempre recomendo definir o valor da chave estrangeira (ProductId) em vez da instância. Também há maneiras de controlar o estado e classificar tudo como EF antes de salvar os dados. Na verdade, minha coluna recente, “Como Lidar com o Estado das Entidades Desconectadas no EF” (msdn.com/magazine/mt694083), mostra um padrão para fazer isso.

Veja outra armadilha comum: as propriedades de navegação que são obrigatórias. Dependendo de como você esteja interagindo com um objeto, talvez não se importe com a propriedade de navegação, mas o EF certamente notará que ela está ausente. Escrevi sobre esse tipo de problema em outra coluna, “Como Sobreviver sem Chaves Estrangeiras” (msdn.com/magazine/hh708747).

Portanto, sim, há soluções alternativas. Mas você também pode aproveitar o padrão CQRS para criar APIs mais limpas e mais explícitas que não exijam soluções alternativas. Isso também significa que elas terão manutenção mais fácil e serão menos propensas a outros efeitos colaterais.

Aplicação do Padrão CQRS a DbContext e a Classes de Domínio

Com frequência, eu usei o padrão CQRS para me ajudar a resolver esse problema. A princípio, isso realmente significa que sejam quais forem os modelos que você esteja dividindo, isso resultará no dobro de classes (embora isso não signifique, necessariamente, o dobro de código). Eu não só crio dois DbContexts separados, como, com frequência, terminarei com pares de classes de domínio, cada um concentrado nas tarefas relevantes em torno de leitura ou de escrita.

Usarei como meu exemplo um modelo que seja uma forma ligeiramente diferente daquela mais simples que já apresentei. Este exemplo vem de uma solução dimensionável que criei para um curso recente da Pluralsight. No modelo, há uma classe SalesOrder que age como a raiz agregada no domínio. Em outras palavras, o tipo SalesOrder controla o que acontece para qualquer um dos outros tipos relacionados na agregação. Ele controla como os LineItems são criados, como os descontos são calculados, como um endereço de remessa é derivado e assim por diante. Se você pensar nas tarefas que acabei de mencionar, elas se concentram mais na criação do pedido. Na realidade, você não precisa se preocupar com as regras da criação de um novo item de linha para um pedido quando estiver simplesmente lendo as informações do pedido no banco de dados.

Por outro lado, ao exibir os dados, pode haver muitas outras informações interessantes do que as que me interessam quando eu estiver enviando dados por push para o banco de dados.

Um Modelo para os Dados Consultados

A Figura 3 mostra o tipo SalesOrder no projeto Order.Read.Domain da minha solução. Existem várias propriedades aqui e apenas um único método para a criação de dados de exibição melhores. Você não vê regras de negócios aqui porque eu não me preocupo com a validação de dados.

Figura 3 O Tipo SalesOrder Definido para Ser Usado em Leituras de Dados

namespace Order.Read.Domain {
 public class SalesOrder : Entity  {
  protected SalesOrder()   {
    LineItems = new List<LineItem>();
  }
  public DateTime OrderDate { get; set; }
  public DateTime? DueDate { get; set; }
  public bool OnlineOrder { get; set; }
  public string PurchaseOrderNumber { get; set; }
  public string Comment { get; set; }
  public int PromotionId { get; set; }
  public Address ShippingAddress { get; set; }
  public CustomerStatus CurrentCustomerStatus { get; set; }
  public double Discount   {
    get { return CustomerDiscount + PromoDiscount; }
  }
  public double CustomerDiscount { get; set; }
  public double PromoDiscount { get; set; }
  public string SalesOrderNumber { get; set; }
  public int CustomerId { get; set; }
  public double SubTotal { get; set; }
  public ICollection<LineItem> LineItems { get; set; }
  public decimal CalculateShippingCost()   {
    // Items, quantity, price, discounts, total weight of item
    // This is the job of a microservice we can call out to
    throw new NotImplementedException();
  }
}

Compare isso ao SalesOrder da Figura 4 que eu defini para cenários onde armazenarei dados do SalesOrder no banco de dado. Seja um novo pedido ou algum que eu esteja editando. Há muito mais lógica de negócios nesta versão. Há um método de fábrica junto com um construtor privado e protegido que garante que um pedido não possa ser criado sem que determinado dado esteja disponível. Existem métodos com lógica e regras para a forma como um novo item de linha pode ser criado para um pedido e, também, para como aplicar um endereço de remessa. Há um método para controlar como e quando um determinado conjunto de detalhes do pedido pode ser modificado.

Figura 4 O Tipo SalesOrder para a Criação e a Atualização de Dados

namespace Order.Write.Domain {
  public class SalesOrder : Entity   {
    private readonly Customer _customer;
    private readonly List<LineItem> _lineItems;
    public static SalesOrder Create(IEnumerable<CartItem>
      cartItems, Customer customer) {
      var order = new SalesOrder(cartItems, customer);
      return order;
    }
    private SalesOrder(IEnumerable<CartItem> cartItems, Customer customer) : this(){
      Id = Guid.NewGuid();
      _customer = customer;
      CustomerId = customer.CustomerId;
      SetShippingAddress(customer.PrimaryAddress);
      ApplyCustomerStatusDiscount();
      foreach (var item in cartItems)
      {
        CreateLineItem(item.ProductId, (double) item.Price, item.Quantity);
      }
      _customer = customer;
    }
    protected SalesOrder() {
      _lineItems = new List<LineItem>();
      Id = Guid.NewGuid();
      OrderDate = DateTime.Now;
    }
    public DateTime OrderDate { get; private set; }
    public DateTime? DueDate { get; private set; }
    public bool OnlineOrder { get; private set; }
    public string PurchaseOrderNumber { get; private set; }
    public string Comment { get; private set; }
    public int PromotionId { get; private set; }
    public Address ShippingAddress { get; private set; }
    public CustomerStatus CurrentCustomerStatus { get; private set; }
    public double Discount{
      get { return CustomerDiscount + PromoDiscount; }
    }
    public double CustomerDiscount { get; private set; }
    public double PromoDiscount { get; private set; }
    public string SalesOrderNumber { get; private set; }
    public int CustomerId { get; private set; }
    public double SubTotal { get; private set; }
    public ICollection<LineItem> LineItems  {
      get { return _lineItems; }
    }
    public void CreateLineItem(int productId, double listPrice, int quantity)
    {
      // NOTE: more rules to be implemented here
      var item = LineItem.Create(Id, productId, quantity, listPrice,
        CustomerDiscount + PromoDiscount);
      _lineItems.Add(item);
    }
    public void SetShippingAddress(Address address) {
      ShippingAddress = Address.Create(address.Street, address.City,
        address.StateProvince, address.PostalCode);
    }
    public bool HasLineItems(){
      return LineItems.Any();
    }
    public decimal CalculateShippingCost() {
      // Items, quantity, price, discounts, total weight of item
      // This is the job of a microservice we can call out to
      throw new NotImplementedException();
    }
    public void ApplyCustomerStatusDiscount() {
      // The guts of this method are in the sample
    }
    public void SetOrderDetails(bool onLineOrder,
      string PONumber, string comment, int promotionId, double promoDiscount){
      OnlineOrder = onLineOrder;
      PurchaseOrderNumber = PONumber;
      Comment = comment;
      PromotionId = promotionId;
      PromoDiscount = promoDiscount;
    }
  }
}

A versão de gravação do SalesOrder é mais complexa. Mas se eu precisar trabalhar na versão de leitura, não terei toda a lógica de gravação diferente no meu caminho. Se você for fã da diretriz que diz que o código legível é um código menos propenso a erros, talvez, como eu, tenha ainda outro motivo para preferir essa separação. E, com certeza, alguém como Young pensaria que até mesmo essa classe carrega lógica demais. Mas, para nossos propósitos, isso servirá.

O padrão CQRS permite que eu me concentre nos problemas de preenchimento de uma classe SalesOrder (que, neste caso, são poucos) e nos problemas da criação de uma classe SalesOrder separadamente ao definir as classes. Essas classes têm algumas coisas em comum. Por exemplo, ambas as versões da classe SalesOrder definem um relacionamento para o tipo LineItem com uma propriedade ICollection<List>.

Agora, vamos dar uma olhada nos modelos de dados. Ou seja, as classes DbContext que eu uso para o acesso a dados.

O OrderReadContext define um único DbSet que é para a entidade SalesOrder:

public DbSet<SalesOrder> Orders { get; set; }

O EF descobre o tipo LineItem relacionado e cria o modelo mostrado na Figura 5. No entanto, como o EF exige que o DbSet seja exposto, ele também possibilita que qualquer um chame OrderReadContext.SaveChanges. É neste ponto que as camadas vão te ajudar. Andrea Saltarello oferece uma ótima maneira de encapsular o DbContext de forma que somente o DbSet seja exposto e os desenvolvedores (ou você, no futuro) que usarem essa classe não terão acesso direto ao OrderReadContext. Isso pode ajudar a evitar a chamada acidental a SaveChanges no modelo de leitura.

O Modelo de Dados Baseado no OrderReadContext
Figura 5 O Modelo de Dados Baseado no OrderReadContext

Um exemplo simples de uma classe como essa é:

public class ReadModel {
  private OrderReadContext readContext = null;
  public ReadModel() {
    readContext = new OrderReadContext();
  }
  public IQueryable<SalesOrder> Orders {
    get {
      return readContext.Orders;
    }
  }
}

Outra proteção que você pode adicionar a essa implementação é aproveitar a vantagem do fato de que SaveChanges é virtual. Você pode substituir SaveChanges para que ele nunca chame o método interno DbContext.SaveChanges.

O OrderWriteContext define dois DbSets: não apenas um para SalesOrder, mas outro para a entidade LineItem:

public DbSet<SalesOrder> Orders { get; set; }
public DbSet<LineItem> LineItems { get; set; }

Isso já é interessante, pois eu não tive de me preocupar em expor um DbSet para LineItems no outro DbContext. No OrderReadContext, consultarei somente por meio de SalesOrders. Nunca consultarei diretamente em LineItems e, portanto, não há necessidade de expor um DbSet para esse tipo. Lembre-se de preencher a janela do WPF na consulta como na Figura 2. Fiz um carregamento adiantado de LineItems por meio do DbSet Orders.

A outra lógica importante no OrderWriteContext é que eu disse explicitamente ao EF para ignorar o relacionamento entre SalesOrder e LineItem usando a API fluente:

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
  modelBuilder.Entity<SalesOrder>().Ignore(s => s.LineItems);
}

O modelo resultante é semelhante ao da Figura 6.

O Modelo de Dados Baseado no OrderWriteContext
Figura 6 O Modelo de Dados Baseado no OrderWriteContext

Isso significa que eu não posso usar o EF para navegar de SalesOrder para LineItem. Isso não me impede de fazer isso em minha lógica de negócios. Como você viu, tenho muito código na classe SalesOrder que interage com LineItems. No entanto, não poderei gravar uma consulta que navegue por meio de LineItems, como context.SalesOrders.Include(s=>s.LineItems). Isso poderá gerar um momento de pânico até eu lembrar você de que esse é o modelo para gravar dados e não para lê-los. O EF pode recuperar os dados relacionados sem problemas usando o OrderReadContext.

Prós e Contras de um DbContext Sem Relacionamentos para Gravações

Assim, o que eu ganhei ao separar as responsabilidades de gravação das responsabilidades de consulta? É fácil ver as desvantagens. Eu tenho mais código para manter. Mais importante, o EF não atualizará magicamente os gráficos para mim. Terei mais trabalho manual para garantir que quando estiver inserindo, atualizando ou excluindo dados, os relacionamentos sejam manipulados de forma adequada. Por exemplo, se você tiver um código que adicione um novo LineItem a SalesOrder, simplesmente gravar myOrder.Line­Items.Add(someItem) não disparará o EF para que ele envie por push orderId para o LineItem quando for a hora de persistir o LineItem no banco de dados. Você terá de definir explicitamente o valor de orderId. Se você examinar novamente o método CreateLineItem de SalesOrder na Figura 4, verá que eu já pensei nisso. No meu sistema, a única maneira de criar um novo item de linha para um pedido é por meio desse método, o que significa que não posso gravar código em qualquer outro lugar que não tenha a etapa crítica de aplicação da orderId. Outra pergunta que você pode fazer é: “E se eu quiser alterar a orderId de um determinado item de linha?” No meu sistema, essa é uma ação que não faz muito sentido. Posso ver a remoção de itens de linha dos pedidos. Posso ver a adição de itens de linha aos pedidos. Mas não há uma regra de negócios que permita a alteração da orderId. Entretanto, não consigo ignorar esses “e se”, já que estou acostumada a simplesmente criar esses recursos no meu modelo de dados.

Além do controle explícito que eu tenho sobre os relacionamentos, a divisão da lógica de leitura e de gravação também me faz pensar em toda a lógica adicionada aos meus modelos de dados por padrão, quando parte da lógica nunca será usada. E essa lógica estranha poderá me forçar a escrever soluções alternativas para evitar seus efeitos colaterais.

Os problemas que levantei anteriormente sobre os dados de referência acidentalmente adicionados ao banco de dados ou sobre os valores nulos introduzidos quando você estiver lendo dados que não pretende atualizar, eles também desaparecerão. Uma classe definida para leitura pode incluir os valores que você deseja ver mas não atualizar. Meu exemplo de SalesOrder não tem este problema em particular. Mas uma classe de gravação poderia evitar a inclusão de propriedades que talvez você queira exibir mas não atualizar e, portanto, evitar a substituição de propriedades ignoradas por valores nulos.

Verifique se o Esforço Vale a Pena

O CQRS pode adicionar muito trabalho ao desenvolvimento do seu sistema. Dê uma olhada nos artigos que oferecem diretrizes sobre quando o CQRS poderia ser excessivo para o problema a ser resolvido, como o de Udi Dahan em bit.ly/2bIbd7i. “CQRS para o Aplicativo Comum”, de Dino Esposito (msdn.com/magazine/mt147237), também oferece informações. Meu uso particular desse padrão não é o que você pode pensar como um CQRS completo, mas receber a “permissão” de dividir as leituras e as gravações do CQRS me ajudou a reduzir a complexidade das soluções em que um modelo de dados que passe dos limites fique no caminho. Encontrar um equilíbrio entre gravar código extra para contornar os efeitos colaterais ou gravar código extra para fornecer caminhos mais claros e mais diretos para a resolução do problema demanda experiência e confiança. Mas, às vezes, o seu instinto é o seu melhor guia.


Julie Lerman é MVP da Microsoft, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos .NET em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do "Programming Entity Framework", bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter em @julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Andrea Saltarello (Managed Designs) (andrea.saltarello@manageddesigns.it)
Andrea Saltarello é um empreendedor e arquiteto de software de Milão, na Itália, que ainda ama gravar código para projetos reais para obter comentários sobre suas decisões de design. Como instrutor e palestrante, ele teve diversos compromissos para ministrar cursos e conferências em toda a Europa, como o TechEd Europe, o DevWeek e o Software Architect. Ele é MVP da Microsoft desde 2003 e recentemente foi nomeado como Diretor Regional da Microsoft. Ele é apaixonado por música e é um grande fã do Depeche Mode, com quem ele tem um caso de amor desde que ouviu “Everything Counts” pela primeira vez.