Condividi tramite


Modello CQRS

Command Query Responsibility Segregation (CQRS) è un modello di progettazione che separa le operazioni di lettura e scrittura per un archivio dati in modelli di dati separati. Questo approccio consente di ottimizzare ogni modello in modo indipendente e può migliorare le prestazioni, la scalabilità e la sicurezza di un'applicazione.

Contesto e problema

In un'architettura tradizionale, un singolo modello di dati viene spesso usato per le operazioni di lettura e scrittura. Questo approccio è semplice ed è adatto per operazioni crud (CRUD) di creazione, lettura, aggiornamento ed eliminazione di base.

Diagramma che mostra un'architettura CRUD tradizionale.

Man mano che le applicazioni aumentano, può diventare sempre più difficile ottimizzare le operazioni di lettura e scrittura in un singolo modello di dati. Le operazioni di lettura e scrittura spesso hanno requisiti di prestazioni e scalabilità diversi. Un'architettura CRUD tradizionale non tiene conto di questa asimmetria, il che può comportare le seguenti sfide:

  • Mancata corrispondenza dei dati: Le rappresentazioni di lettura e scrittura dei dati spesso differiscono. Alcuni campi necessari durante gli aggiornamenti potrebbero non essere necessari durante le operazioni di lettura.

  • contesa di blocco: operazioni parallele sullo stesso set di dati possono causare conflitti di blocco.

  • Problemi di prestazioni: 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.

  • Problemi di sicurezza: Può essere difficile gestire la sicurezza quando le entità sono soggette a operazioni di lettura e scrittura. Questa sovrapposizione può esporre i dati in contesti imprevisti.

La combinazione di queste responsabilità può comportare un modello eccessivamente complicato.

Soluzione

Usare il modello CQRS per separare le operazioni di scrittura o i comandi, dalle operazioni di lettura o dalle query. I comandi aggiornano i dati. Le interrogazioni recuperano i dati. Il modello CQRS è utile negli scenari che richiedono una netta separazione tra comandi e letture.

  • Comprendere i comandi. I comandi devono rappresentare attività aziendali specifiche anziché aggiornamenti di dati di basso livello. Ad esempio, in un'app di prenotazione di hotel usare il comando "Book hotel room" invece di "Set ReservationStatus to Reserved". Questo approccio acquisisce meglio la finalità dell'utente e allinea i comandi ai processi aziendali. Per assicurarsi che i comandi siano riusciti, potrebbe essere necessario perfezionare il flusso di interazione dell'utente e la logica lato server e prendere in considerazione l'elaborazione asincrona.

    Area di perfezionamento Raccomandazione
    Validazione lato client Convalidare condizioni specifiche prima di inviare il comando per evitare errori evidenti. Ad esempio, se non sono disponibili camere, disabilitare il pulsante "Book" e fornire un messaggio chiaro e descrittivo nell'interfaccia utente che spiega perché la prenotazione non è possibile. Questa configurazione riduce le richieste server non necessarie e fornisce feedback immediato agli utenti, migliorando così l'esperienza.
    Logica del lato server Migliorare la logica di business per gestire correttamente i casi perimetrali e gli errori. Ad esempio, per risolvere le condizioni di competizione, come quando più utenti tentano di prenotare l'ultima stanza disponibile, è consigliabile aggiungere gli utenti a una lista di attesa o suggerire alternative.
    Elaborazione asincrona Elaborare i comandi in modo asincrono inserendoli in una coda, anziché gestirli in modo sincrono.
  • Informazioni sulle query. Le query non modificano mai i dati. Restituiscono invece oggetti DTO (Data Transfer Objects) che presentano i dati necessari in un formato pratico, senza logica di dominio. Questa separazione distinta delle responsabilità semplifica la progettazione e l'implementazione del sistema.

Separare i modelli di lettura e scrivere modelli

La separazione del modello di lettura dal modello di scrittura semplifica la progettazione e l'implementazione del sistema risolvendo problemi specifici per le operazioni di scrittura e lettura dei dati. Questa separazione migliora chiarezza, scalabilità e prestazioni, ma introduce compromessi. Ad esempio, gli strumenti di scaffolding come framework O/RM (Object Relational Mapping) non possono generare automaticamente codice CQRS da uno schema di database, quindi è necessaria logica personalizzata per colmare il divario.

Le sezioni seguenti descrivono due approcci principali per implementare la separazione dei modelli di lettura e scrittura in CQRS. Ogni approccio presenta vantaggi e sfide univoci, ad esempio la gestione della sincronizzazione e della coerenza.

Separare i modelli in un singolo archivio dati

Questo approccio rappresenta il livello di base di CQRS, in cui i modelli di lettura e scrittura condividono un singolo database sottostante ma mantengono una logica distinta per le operazioni. Un'architettura CQRS di base consente di delineare il modello di scrittura dal modello di lettura mentre si basa su un archivio dati condiviso.

Diagramma che mostra un'architettura CQRS di base.

Questo approccio migliora chiarezza, prestazioni e scalabilità definendo modelli distinti per la gestione dei problemi di lettura e scrittura.

  • Un modello di scrittura è progettato per gestire i comandi che aggiornano o salvano in modo permanente i dati. Include la convalida e la logica di dominio e consente di garantire la coerenza dei dati ottimizzando l'integrità transazionale e i processi aziendali.

  • Un modello di lettura è progettato per gestire le query per il recupero dei dati. Si concentra sulla generazione di DTO o proiezioni ottimizzate per il livello di presentazione. Migliora le prestazioni delle query e la velocità di risposta evitando la logica di dominio.

Separare i modelli in archivi dati diversi

Un'implementazione più avanzata di CQRS usa archivi dati distinti per i modelli di lettura e scrittura. La separazione degli archivi dati di lettura e scrittura consente di ridimensionare ogni modello in modo che corrisponda al carico. Consente inoltre di usare una tecnologia di archiviazione diversa per ogni archivio dati. È possibile usare un database di documenti per l'archivio dati di lettura e un database relazionale per l'archivio dati di scrittura.

Diagramma che mostra un'architettura CQRS con archivi dati di lettura separati e archivi dati di scrittura.

Quando si usano archivi dati separati, è necessario assicurarsi che entrambi rimangano sincronizzati. Un modello comune consiste nell'avere gli eventi di pubblicazione del modello di scrittura quando aggiorna il database, che il modello di lettura usa per aggiornare i dati. Per altre informazioni su come usare gli eventi, vedere Stile dell'architettura basata su eventi. Poiché in genere non è possibile integrare broker di messaggi e database in una singola transazione distribuita, è possibile che si verifichino problemi di coerenza quando si aggiorna il database e si pubblicano eventi. Per altre informazioni, vedere 'elaborazione di messaggi Idempotenti.

L'archivio dati di lettura può usare uno schema di dati personalizzato ottimizzato per le query. Ad esempio, può archiviare una vista materializzata dei dati per evitare join complessi o mapping O/RM. L'archivio dati di lettura può essere una replica di sola lettura dell'archivio di scrittura o avere una struttura diversa. La distribuzione di più repliche di sola lettura può migliorare le prestazioni riducendo la latenza e aumentando la disponibilità, soprattutto negli scenari distribuiti.

Vantaggi di CQRS

  • Scalabilità indipendente. CQRS consente di leggere i modelli e scrivere modelli in modo indipendente. Questo approccio consente di ridurre al minimo i conflitti di blocco e migliorare le prestazioni del sistema sotto carico.

  • Schemi di dati ottimizzati. Le operazioni di lettura possono usare uno schema ottimizzato per le query. Le operazioni di scrittura usano uno schema ottimizzato per gli aggiornamenti.

  • Sicurezza. Separando le operazioni di lettura e scrittura, è possibile assicurarsi che solo le entità o le operazioni di dominio appropriate dispongano dell'autorizzazione per eseguire azioni di scrittura sui dati.

  • Separazione delle preoccupazioni. Separare le responsabilità di lettura e scrittura comporta modelli più puliti e gestibili. Il lato della scrittura gestisce in genere una complessa logica di business. La parte di lettura può rimanere semplice e focalizzata sull'efficienza delle query.

  • Interrogazioni più semplici. Quando si archivia una vista materializzata nel database di lettura, l'applicazione può evitare join complessi quando esegue query.

Problemi e considerazioni

Quando si decide come implementare questo modello, tenere presente quanto segue:

  • Maggiore complessità. Il concetto di base di CQRS è semplice, ma può introdurre una complessità significativa nella progettazione dell'applicazione, in particolare se combinata con il modello di origine eventi.

  • Problemi di messaggistica. La messaggistica non è un requisito per CQRS, ma viene spesso usata per elaborare i comandi e pubblicare gli eventi di aggiornamento. Quando viene inclusa la messaggistica, il sistema deve tenere conto di potenziali problemi, ad esempio errori dei messaggi, duplicati e tentativi. Per altre informazioni sulle strategie per gestire i comandi con priorità diverse, vedere Code di priorità.

  • Coerenza finale. Quando i database di lettura e scrittura sono separati, i dati di lettura potrebbero non mostrare immediatamente le modifiche più recenti. Questo ritardo comporta dati non aggiornati. Assicurarsi che l'archivio modelli di lettura rimanga up-to-date con le modifiche nell'archivio modelli di scrittura può risultare complesso. Inoltre, il rilevamento e la gestione di scenari in cui un utente agisce su dati non aggiornati richiede un'attenta considerazione.

Quando usare questo modello

Usare questo modello quando:

  • Si lavora in ambienti collaborativi. Negli ambienti in cui più utenti accedono e modificano contemporaneamente gli stessi dati, CQRS consente di ridurre i conflitti di merge. I comandi possono includere una granularità sufficiente per evitare conflitti e il sistema può risolvere eventuali conflitti che si verificano all'interno della logica del comando.

  • Sono disponibili interfacce utente basate su attività. Le applicazioni che guidano gli utenti attraverso processi complessi come una serie di passaggi o con modelli di dominio complessi traggono vantaggio da CQRS.

    • Il modello di scrittura dispone di uno stack completo per l'elaborazione dei comandi con logica aziendale, validazione degli input e validazione aziendale. Il modello di scrittura può considerare un set di oggetti associati come una singola unità per le modifiche ai dati, noto come aggregazione nella terminologia di progettazione basata su dominio. Il modello di scrittura può anche contribuire a garantire che questi oggetti siano sempre in uno stato coerente.

    • Il modello di lettura non ha una logica aziendale o un sistema di convalida. Restituisce un oggetto DTO da usare in un modello di visualizzazione. Il modello di lettura è coerente con il modello di scrittura.

  • È necessaria l'ottimizzazione delle prestazioni. I sistemi in cui le prestazioni delle letture dei dati devono essere ottimizzate separatamente dalle prestazioni delle scritture di dati traggono vantaggio da CQRS. Questo modello è particolarmente utile quando il numero di letture è maggiore del numero di scritture. Il modello di lettura viene ridimensionato orizzontalmente per gestire volumi di query di grandi dimensioni. Il modello di scrittura viene eseguito in meno istanze per ridurre al minimo i conflitti di unione e mantenere la coerenza.

  • Hai la separazione dei problemi di sviluppo. CQRS consente ai team di lavorare in modo indipendente. Un team implementa la logica di business complessa nel modello di scrittura e un altro team sviluppa i componenti del modello di lettura e dell'interfaccia utente.

  • Hai dei sistemi in evoluzione. CQRS supporta sistemi che si evolvono nel tempo. Supporta nuove versioni del modello, modifiche frequenti alle regole business o altre modifiche senza influire sulle funzionalità esistenti.

  • È necessaria l'integrazione del sistema: I sistemi che si integrano con altri sottosistemi, in particolare i sistemi che usano il modello di origine eventi, rimangono disponibili anche se un sottosistema ha esito negativo temporaneamente. CQRS isola gli errori, che impediscono a un singolo componente di influire sull'intero sistema.

Questo modello potrebbe non essere adatto 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.

Progettazione del carico di lavoro

Valutare come usare il modello CQRS nella progettazione di un carico di lavoro per soddisfare gli obiettivi e i principi trattati nei pilastri di Azure Well-Architected Framework. La tabella seguente fornisce indicazioni sul modo in cui questo modello supporta gli obiettivi del pilastro Efficienza delle prestazioni.

Pilastro 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 nel ridimensionamento, nei dati e nel 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 il ridimensionamento mirati per lo scopo specifico di ogni operazione.

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

Prendere in considerazione eventuali compromessi rispetto agli obiettivi degli altri pilastri che questo modello potrebbe introdurre.

Combinare i pattern di Event Sourcing e CQRS

Alcune implementazioni di CQRS incorporano il modello di origine eventi. Questo modello archivia lo stato del sistema come una serie cronologica di eventi. Ogni evento acquisisce le modifiche apportate ai dati in un momento specifico. Per determinare lo stato corrente, il sistema riproduce questi eventi in ordine. In questa configurazione:

  • L'archivio eventi è il modello di scrittura e l'unica fonte di verità.

  • Il modello letto genera viste materializzate da questi eventi, in genere in un formato altamente denormalizzato. Queste viste ottimizzano il recupero dei dati adattando le strutture alle esigenze di query e visualizzazione.

Vantaggi della combinazione dei modelli di Event Sourcing e CQRS

Gli stessi eventi che aggiornano il modello di scrittura possono fungere da input per il modello di lettura. Il modello di lettura può quindi creare uno snapshot in tempo reale dello stato corrente. Questi snapshot ottimizzano le query fornendo visualizzazioni efficienti e pre-calcolate dei dati.

Anziché archiviare direttamente lo stato corrente, il sistema usa un flusso di eventi come archivio di scrittura. Questo approccio riduce i conflitti di aggiornamento sulle aggregazioni e migliora le prestazioni e la scalabilità. Il sistema può elaborare questi eventi in modo asincrono per compilare o aggiornare viste materializzate per l'archivio dati di lettura.

Poiché l'archivio eventi funge da singola fonte di verità, è possibile rigenerare facilmente le visualizzazioni materializzate o adattarsi alle modifiche nel modello di lettura riproducendo gli eventi cronologici. Fondamentalmente, le viste materializzate funzionano come una cache durevole di sola lettura ottimizzata per query veloci ed efficienti.

Considerazioni su come combinare i modelli di Event Sourcing e CQRS.

Prima di combinare il modello CQRS con il modello di origine eventi , valutare le considerazioni seguenti:

  • Coerenza finale: Poiché gli archivi dati di scrittura e lettura sono separati, gli aggiornamenti dell'archivio dati di lettura potrebbero causare un ritardo nella generazione di eventi. Questo ritardo comporta la coerenza finale.

  • Maggiore complessità: La combinazione del modello CQRS con il modello di origine eventi richiede un approccio di progettazione diverso, che può rendere un'implementazione di successo più complessa. È necessario scrivere codice per generare, elaborare e gestire gli eventi e assemblare o aggiornare le viste per il modello di lettura. Tuttavia, il modello di origine eventi semplifica la modellazione del dominio e consente di ricompilare o creare nuove visualizzazioni facilmente conservando la cronologia e la finalità di tutte le modifiche ai dati.

  • Prestazioni della generazione di viste: La generazione di viste materializzate per il modello di lettura può richiedere tempo e risorse notevoli. Lo stesso vale per proiettare i dati riproducendo ed elaborando eventi per entità o raccolte specifiche. La complessità aumenta quando i calcoli comportano l'analisi o la somma dei valori in lunghi periodi, perché è necessario esaminare tutti gli eventi correlati. Implementare le istantanee dei dati a intervalli regolari. Ad esempio, archiviare lo stato corrente di un'entità o snapshot periodici di totali aggregati, ovvero il numero di volte in cui si verifica un'azione specifica. Gli snapshot riducono la necessità di elaborare ripetutamente la cronologia eventi completa, migliorando così le prestazioni.

Esempio

Il codice seguente mostra gli estratti da un esempio di implementazione di CQRS che usa definizioni diverse per i modelli di lettura e i modelli di scrittura. Le interfacce del modello non determinano le funzionalità degli archivi dati sottostanti e possono evolversi e 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 esegue questa operazione usando il RateProduct comando 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 ProductsCommandHandler classe 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)
  {
    ...
  }
}

Passaggio successivo

Quando si implementa questo modello, è possibile che le informazioni seguenti siano rilevanti:

  • Le linee guida per il partizionamento dei dati descrivono le procedure consigliate per suddividere i dati in partizioni che è possibile gestire e accedere separatamente per migliorare la scalabilità, ridurre i conflitti e ottimizzare le prestazioni.
  • Event Sourcing pattern. Questo modello descrive come semplificare le attività in domini complessi e migliorare prestazioni, scalabilità e velocità di risposta. Viene inoltre illustrato come garantire la coerenza per i dati transazionali mantenendo allo stesso tempo audit trail completi e cronologia per abilitare azioni di compensazione.

  • Modello di vista materializzata. Questo modello crea viste prepopolate, note come viste materializzate, per eseguire query efficienti ed estrarre dati da uno o più archivi dati. 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.