Modello CQRS

Archiviazione di Azure

CQRS è l'acronimo di Command and Query Responsibility Segregation, un modello che separa le operazioni di lettura e aggiornamento per un archivio dati. L'implementazione del modello CQRS nell'applicazione può massimizzare prestazioni, scalabilità e sicurezza. La flessibilità creata dalla migrazione al modello CQRS consente a un sistema di evolversi meglio nel tempo e impedisce ai comandi di aggiornamento di causare conflitti di merge a livello di dominio.

Contesto e problema

Nelle architetture tradizionali viene usato lo stesso modello di dati per eseguire query su un database e per aggiornarlo. Questo è un comportamento semplice che funziona bene per operazioni CRUD di base. In applicazioni più complesse, tuttavia, questo approccio può risultare poco pratico. Ad esempio, sul lato lettura l'applicazione può eseguire molte query diverse, restituendo oggetti DTO (Data Transfer Object) in diverse forme. In questi casi, il mapping degli oggetti può diventare difficoltoso. Sul lato scrittura il modello può implementare convalida e logica di business complesse. Di conseguenza, può risultare un modello eccessivamente complesso che esegue troppe attività.

I carichi di lavoro di lettura e scrittura sono spesso asimmetrici, con requisiti di prestazioni e scalabilità molto diversi.

Architettura CRUD tradizionale

  • Spesso esiste una mancata corrispondenza tra le rappresentazioni di lettura e scrittura dei dati, ad esempio colonne o proprietà aggiuntive che devono essere aggiornate correttamente anche se non sono necessarie come parte di un'operazione.

  • La contesa dei dati può verificarsi quando le operazioni vengono eseguite in parallelo sullo stesso set di dati.

  • L'approccio tradizionale può avere un effetto negativo sulle prestazioni a causa del carico sul livello di accesso ai dati e dell'archivio dati e la complessità delle query necessarie per recuperare le informazioni.

  • La gestione della sicurezza e delle autorizzazioni può diventare complessa, perché ogni entità è soggetta a operazioni di lettura e scrittura, che potrebbero esporre i dati nel contesto errato.

Soluzione

CQRS separa le letture e le scritture in modelli diversi, usando i comandi per aggiornare i dati e le query per leggere i dati.

  • I comandi devono essere basati su attività, anziché incentrati sui dati. ("Prenota camera d'albergo", non "imposta ReservationStatus su Riservato"). Ciò potrebbe richiedere alcune modifiche corrispondenti allo stile di interazione dell'utente. L'altra parte di questa operazione consiste nell'esaminare la modifica della logica di business che elabora tali comandi in modo da avere esito positivo più frequentemente. Una tecnica che supporta questa operazione consiste nell'eseguire alcune regole di convalida nel client anche prima di inviare il comando, eventualmente disabilitando i pulsanti, spiegando perché nell'interfaccia utente ("nessuna stanza lasciata"). In questo modo, la causa degli errori dei comandi sul lato server può essere limitata alle condizioni di race condition (due utenti che tentano di prenotare l'ultima stanza) e anche quelli possono talvolta essere risolti con altri dati e logica (inserendo un ospite in una lista di attesa).
  • I comandi possono essere inseriti in una coda per l'elaborazione asincrona, anziché essere elaborati in modo sincrono.
  • Le query non modificano mai il database. Una query restituisce un oggetto DTO che non incapsula informazioni sul dominio.

I modelli possono quindi essere isolati, come illustrato nel diagramma seguente, anche se non è un requisito assoluto.

Architettura CQRS di base

La presenza di modelli di query e aggiornamento separati semplifica la progettazione e l'implementazione. Tuttavia, uno svantaggio è che il codice CQRS non può essere generato automaticamente da uno schema di database usando meccanismi di scaffolding come gli strumenti O/RM (tuttavia, sarà possibile compilare la personalizzazione sopra il codice generato).

Per un isolamento maggiore, è possibile separare fisicamente i dati di lettura da quelli di scrittura. In questo caso, il database di lettura può usare il proprio schema dei dati ottimizzato per le query. Ad esempio, può archiviare una vista materializzata dei dati per evitare join complessi o mapping relazionali di oggetti. Può addirittura usare un tipo di archivio dati diverso. Ad esempio, il database di scrittura può essere relazionale, mentre quello di lettura può essere un database di documenti.

Se vengono usati database di lettura e scrittura separati, devono essere mantenuti sincronizzati. Questa operazione viene in genere eseguita tramite la pubblicazione di un evento da parte del modello di scrittura ogni volta che aggiorna il database. Per altre informazioni sull'uso degli eventi, vedere Stile dell'architettura basata su eventi. Poiché i broker di messaggi e i database in genere non possono essere inseriti in una singola transazione distribuita, possono verificarsi problemi di coerenza durante l'aggiornamento del database e la pubblicazione di eventi. Per altre informazioni, vedere le indicazioni sull'elaborazione dei messaggi idempotenti.

Architettura CQRS con archivi di lettura e scrittura separati

L'archivio di lettura può essere una replica di sola lettura di quello di scrittura oppure gli archivi di lettura e scrittura possono avere una struttura completamente diversa. L'uso di più repliche di sola lettura può migliorare le prestazioni delle query, in particolare negli scenari distribuiti in cui le repliche di sola lettura si trovano vicino alle istanze dell'applicazione.

La separazione degli archivi di lettura e scrittura consente anche di dimensionare ogni archivio a seconda del carico. Il carico degli archivi di lettura, ad esempio, è in genere molto più elevato rispetto a quello degli archivi di scrittura.

Alcune implementazioni di CQRS usano il modello di determinazione dell'origine degli eventi. Con questo modello lo stato dell'applicazione viene archiviato come sequenza di eventi. Ogni evento rappresenta un set di modifiche apportate ai dati. Lo stato corrente viene costruito riproducendo gli eventi. In un contesto CQRS, un vantaggio di Event Sourcing è che gli stessi eventi possono essere usati per notificare ad altri componenti, in particolare, per notificare al modello di lettura. Il modello di lettura usa gli eventi per creare uno snapshot dello stato corrente, più efficiente per le query. Tuttavia, la determinazione dell'origine degli eventi aggiunge complessità alla progettazione.

I vantaggi di CQRS includono:

  • Scalabilità indipendente. CQRS consente il ridimensionamento indipendente dei carichi di lavoro di lettura e scrittura e può ridurre i conflitti di blocco.
  • Schemi di dati ottimizzati. Il lato lettura può usare uno schema ottimizzato per le query, mentre il lato scrittura userà uno schema ottimizzato per gli aggiornamenti.
  • Sicurezza. È più facile fare in modo che solo le entità di dominio corrette eseguano scritture sui dati.
  • Separazione delle attività. L'isolamento del lato lettura dal lato scrittura e viceversa può comportare modelli più gestibili e flessibili. La maggior parte della logica di business è correlata al modello di scrittura. Il modello di lettura può essere relativamente semplice.
  • Query più semplici. Grazie all'archiviazione di una vista materializzata nel database di lettura, l'applicazione può evitare join complessi durante l'esecuzione di query.

Problemi di implementazione e considerazioni

Alcune sfide dell'implementazione di questo modello includono:

  • Complessità. L'idea alla base di CQRS è semplice. Tuttavia, può aggiungere complessità alla progettazione di applicazioni, in particolare quando si usa il modello di determinazione dell'origine degli eventi.

  • Messaggistica. Benché CQRS non richieda la messaggistica, questa viene comunemente usata per elaborare i comandi e pubblicare gli eventi di aggiornamento. In questo caso, l'applicazione deve gestire gli errori dei messaggi o i messaggi duplicati. Vedere le indicazioni sulle code di priorità per gestire i comandi con priorità diverse.

  • Coerenza finale. Separando i database di lettura e scrittura, i dati di lettura possono non essere aggiornati. L'archivio modelli di lettura deve essere aggiornato per riflettere le modifiche apportate all'archivio modelli di scrittura e può essere difficile rilevare quando un utente ha emesso una richiesta in base ai dati di lettura non aggiornati.

Quando usare il modello CQRS

Prendere in considerazione CQRS per gli scenari seguenti:

  • Domini collaborativi in cui molti utenti accedono agli stessi dati in parallelo. CQRS consente di definire comandi con una granularità sufficiente per ridurre al minimo i conflitti di merge a livello di dominio e i conflitti che si verificano possono essere uniti dal comando.

  • Interfacce utente basate su attività in cui gli utenti vengono guidati in un processo complesso, ad esempio con una serie di passaggi o con modelli di dominio complessi. Il modello di scrittura ha uno stack completo di elaborazione dei comandi con logica di business, convalida dell'input e convalida aziendale. Il modello di scrittura può considerare un set di oggetti associati come una singola unità per le modifiche ai dati (un'aggregazione, nella terminologia DDD) e assicurarsi che questi oggetti siano sempre in uno stato coerente. Il modello di lettura non ha una logica di business o uno stack di convalida e restituisce solo un DTO da usare in un modello di visualizzazione. Il modello di lettura è coerente con il modello di scrittura.

  • Gli scenari in cui le prestazioni delle letture dei dati devono essere ottimizzate separatamente dalle prestazioni delle scritture di dati, soprattutto quando il numero di letture è molto maggiore del numero di scritture. In questo scenario è possibile aumentare il numero di istanze del modello di lettura, ma eseguire il modello di scrittura in poche istanze. Un numero ridotto di istanze di un modello di scrittura consente anche di ridurre l'occorrenza di conflitti di unione.

  • Scenari in cui un team di sviluppatori possa concentrarsi su un modello di dominio complesso che fa parte del modello di scrittura e in cui un altro team possa concentrarsi sul modello di lettura e sulle interfacce utente.

  • Scenari in cui si prevede che il sistema si evolva nel tempo e che possa contenere più versioni del modello oppure in cui le regole di business cambiano regolarmente.

  • Integrazione con altri sistemi, soprattutto in combinazione con un'origine eventi, in cui l'errore temporaneo di un sottosistema non deve influire sulla disponibilità degli altri.

Questo modello non è consigliato quando:

  • Il dominio o le regole business sono semplici.

  • Un'interfaccia utente di tipo CRUD semplice e le operazioni di accesso ai dati sono sufficienti.

È possibile applicare il modello CQRS a sezioni limitate del sistema in cui è più rilevante.

Progettazione del carico di lavoro

Un architetto deve valutare il modo in cui il modello CQRS può essere usato nella progettazione del carico di lavoro per soddisfare gli obiettivi e i principi trattati nei pilastri di Azure Well-Architected Framework. Ad esempio:

Concetto fondamentale Come questo modello supporta gli obiettivi di pilastro
L'efficienza delle prestazioni consente al carico di lavoro di soddisfare in modo efficiente le richieste tramite ottimizzazioni in termini di scalabilità, dati, codice. La separazione delle operazioni di lettura e scrittura in carichi di lavoro con operazioni di lettura/scrittura elevate consente di ottimizzare le prestazioni e la scalabilità mirate per lo scopo specifico di ogni operazione.

- PE:05 Ridimensionamento e partizionamento
- Prestazioni dei dati PE:08

Come per qualsiasi decisione di progettazione, prendere in considerazione eventuali compromessi rispetto agli obiettivi degli altri pilastri che potrebbero essere introdotti con questo modello.

Modello di origine eventi e CQRS

Il modello CQRS viene spesso usato con il modello di origine evento. I sistemi basati su CQRS usano modelli dati di lettura e scrittura separati, ognuno personalizzato in base ad attività rilevanti e spesso situato in archivi separati fisicamente. Quando vengono usati con il modello di origine eventi, l'archivio degli eventi è il modello di scrittura e l'origine ufficiale delle informazioni. Il modello di lettura di un sistema basato su CQRS offre viste materializzate dei dati, in genere come viste fortemente denormalizzate. Tali viste sono specifiche per le interfacce e i requisiti di visualizzazione dell'applicazione, in modo da ottimizzare sia le prestazioni delle query che quelle di visualizzazione.

L'uso del flusso di eventi come archivio di scrittura, anziché dei dati effettivi in un punto nel tempo, consente di evitare conflitti di aggiornamento su una singola funzione di aggregazione e di ottimizzare prestazioni e scalabilità. Gli eventi possono essere usati per generare in modo asincrono le viste materializzate dei dati usati per popolare l'archivio di lettura.

Poiché l'archivio di eventi è l'origine ufficiale delle informazioni, è possibile eliminare le viste materializzate e riprodurre tutti gli eventi precedenti per creare una nuova rappresentazione dello stato corrente quando il sistema si evolve o quando il modello di lettura deve cambiare. Le viste materializzate sono in effetti una cache di sola lettura permanente dei dati.

Quando si usa il modello CQRS in combinazione con il modello di origine evento, è necessario considerare quanto segue:

  • In modo analogo a qualsiasi sistema in cui gli archivi di lettura e scrittura sono separati, i sistemi basati su questo modello sono caratterizzati solo da coerenza finale. Si verifica pertanto un ritardo tra l'evento generato e l'archivio dati aggiornato.

  • Il modello aggiunge complessità perché è necessario creare codice per avviare e gestire gli eventi e assemblare o aggiornare le viste o gli oggetti appropriati richiesti dalle query o da un modello di lettura. La complessità del modello CQRS quando si usa il modello di origine evento rende più difficile una corretta implementazione e richiede un approccio diverso alla progettazione di sistemi. Il modello di origine evento, tuttavia, può semplificare la modellazione del dominio e la rigenerazione di viste o la creazione di nuove perché lo scopo delle modifiche dei dati viene mantenuto.

  • La generazione di viste materializzare per l'uso nel modello di lettura o nelle proiezioni dei dati per la riproduzione e la gestione degli eventi per entità specifiche o raccolte di entità può richiedere tempo di elaborazione e uso di risorse significativi. Questo aspetto è particolarmente vero se è richiesta la somma o l'analisi dei valori per lunghi periodi, poiché potrebbe essere necessario esaminare tutti gli eventi associati. Risolvere questo problema implementando snapshot dei dati a intervalli pianificati, ad esempio un conteggio totale del numero di un'azione specifica che si è verificata o lo stato corrente di un'entità.

Esempio di modello CQRS

Il codice seguente mostra alcuni estratti da un esempio di un'implementazione del modello CQRS che usa definizioni diverse per modelli di lettura e di scrittura. Le interfacce di modello non impongono alcuna funzionalità degli archivi dati sottostanti e possono evolversi ed essere ottimizzate in modo indipendente, perché queste interfacce sono separate.

Il codice seguente illustra la definizione di modello di lettura.

// Query interface
namespace ReadModel
{
  public interface ProductsDao
  {
    ProductDisplay FindById(int productId);
    ICollection<ProductDisplay> FindByName(string name);
    ICollection<ProductInventory> FindOutOfStockProducts();
    ICollection<ProductDisplay> FindRelatedProducts(int productId);
  }

  public class ProductDisplay
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal UnitPrice { get; set; }
    public bool IsOutOfStock { get; set; }
    public double UserRating { get; set; }
  }

  public class ProductInventory
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int CurrentStock { get; set; }
  }
}

Il sistema consente agli utenti di valutare i prodotti. Il codice dell'applicazione può usare il comando RateProduct illustrato nel codice seguente.

public interface ICommand
{
  Guid Id { get; }
}

public class RateProduct : ICommand
{
  public RateProduct()
  {
    this.Id = Guid.NewGuid();
  }
  public Guid Id { get; set; }
  public int ProductId { get; set; }
  public int Rating { get; set; }
  public int UserId {get; set; }
}

Il sistema usa la classe ProductsCommandHandler per gestire i comandi inviati dall'applicazione. I client inviano in genere comandi al dominio usando un sistema di messaggistica, ad esempio una coda. Il gestore del comando accetta tali comandi e richiama i metodi dell'interfaccia di dominio. La granularità di ogni comando è progettata per ridurre il rischio di richieste in conflitto. Il codice seguente illustra la classe ProductsCommandHandler.

public class ProductsCommandHandler :
    ICommandHandler<AddNewProduct>,
    ICommandHandler<RateProduct>,
    ICommandHandler<AddToInventory>,
    ICommandHandler<ConfirmItemShipped>,
    ICommandHandler<UpdateStockFromInventoryRecount>
{
  private readonly IRepository<Product> repository;

  public ProductsCommandHandler (IRepository<Product> repository)
  {
    this.repository = repository;
  }

  void Handle (AddNewProduct command)
  {
    ...
  }

  void Handle (RateProduct command)
  {
    var product = repository.Find(command.ProductId);
    if (product != null)
    {
      product.RateProduct(command.UserId, command.Rating);
      repository.Save(product);
    }
  }

  void Handle (AddToInventory command)
  {
    ...
  }

  void Handle (ConfirmItemsShipped command)
  {
    ...
  }

  void Handle (UpdateStockFromInventoryRecount command)
  {
    ...
  }
}

Passaggi successivi

Quando si implementa questo modello, possono essere utili i modelli e le linee guida seguenti:

Post di blog di Martin Fowler:

  • Modello di origine eventi. Descrive in dettaglio il modo in cui l'origine evento può essere usata con il modello CQRS per semplificare le attività in domini complessi e migliorare nel contempo prestazioni, scalabilità e velocità di risposta. Descrive anche come offrire coerenza per i dati transazionali e mantenere controlli completi e cronologia che possono abilitare le azioni di compensazione.

  • Modello di vista materializzata. Il modello di lettura di un'implementazione CQRS può contenere viste materializzate dei dati del modello di scrittura oppure può essere usato per generare viste materializzate.

  • Presentazione su CQRS migliori tramite modelli di interazione utente asincroni