Pola CQRS

Azure Storage

CQRS adalah singkatan dari Pemisahan Tanggung Jawab Kueri dan Perintah, sebuah pola yang memisahkan operasi membaca dan memperbarui untuk penyimpanan data. Menerapkan CQRS dalam aplikasi Anda dapat memaksimalkan performa, skalabilitas, dan keamanannya. Fleksibilitas yang dibuat dengan bermigrasi ke CQRS memungkinkan sistem berkembang lebih baik dari waktu ke waktu dan mencegah perintah pembaruan menyebabkan konflik penggabungan di tingkat domain.

Konteks dan masalah

Dalam arsitektur tradisional, model data yang sama digunakan untuk kueri dan memperbarui database. Hal tersebut sederhana dan bekerja dengan baik untuk operasi CRUD dasar. Dalam aplikasi yang lebih kompleks, bagaimanapun, pendekatan ini bisa menjadi berat. Misalnya, di sisi baca, aplikasi dapat melakukan banyak kueri yang berbeda, mengembalikan objek transfer data (DTO) dengan bentuk yang berbeda. Pemetaan objek bisa menjadi rumit. Di sisi tulis, model dapat menerapkan validasi kompleks dan logika bisnis. Akibatnya, Anda dapat berakhir dengan model yang terlalu kompleks yang melakukan terlalu banyak.

Beban kerja baca dan tulis sering kali asimetris, dengan persyaratan performa dan skala yang sangat berbeda.

Arsitektur CRUD tradisional

  • Sering ada ketidakcocokan antara representasi baca dan tulis data, seperti kolom atau properti tambahan yang harus diperbarui dengan benar meskipun tidak diperlukan sebagai bagian dari operasi.

  • Pertentangan data dapat terjadi ketika operasi dilakukan secara paralel pada kumpulan data yang sama.

  • Pendekatan tradisional dapat memiliki efek negatif pada performa karena beban pada penyimpanan data dan lapisan akses data, dan kompleksitas kueri yang diperlukan untuk mengambil informasi.

  • Mengelola keamanan dan izin dapat menjadi kompleks, karena setiap entitas tunduk pada operasi baca dan tulis, yang mungkin mengekspos data dalam konteks yang salah.

Solusi

CQRS memisahkan pembacaan dan penulisan ke dalam model yang berbeda, menggunakan perintah untuk memperbarui data, dan kueri untuk membaca data.

  • Perintah harus berbasis tugas, bukan data sentris. ("Pesan kamar hotel", bukan "atur ReservationStatus ke Dipesan"). Ini mungkin memerlukan beberapa perubahan yang sesuai dengan gaya interaksi pengguna. Bagian lain dari itu adalah melihat memodifikasi logika bisnis yang memproses perintah tersebut agar lebih sering berhasil. Salah satu teknik yang mendukung ini adalah menjalankan beberapa aturan validasi pada klien bahkan sebelum mengirim perintah, mungkin menonaktifkan tombol, menjelaskan mengapa di UI ("tidak ada ruang tersisa"). Dengan cara itu, penyebab kegagalan perintah sisi server dapat dipersempit ke kondisi balapan (dua pengguna mencoba memesan ruang terakhir), dan bahkan yang kadang-kadang dapat diatasi dengan beberapa data dan logika lagi (menempatkan tamu di daftar tunggu).
  • Perintah dapat ditempatkan pada antrean untuk pemrosesan asinkron, daripada diproses secara sinkron.
  • Kueri tidak pernah mengubah database. Kueri mengembalikan DTO yang tidak merangkum pengetahuan domain apa pun.

Model kemudian dapat diisolasi, seperti yang ditunjukkan pada diagram berikut, meskipun itu bukan persyaratan mutlak.

Arsitektur CQRS dasar

Memiliki model kueri dan pembaruan terpisah menyederhanakan desain dan penerapan. Namun, salah satu kerugiannya adalah bahwa kode CQRS tidak dapat secara otomatis dihasilkan dari skema database menggunakan mekanisme perancah seperti alat O/RM (Namun, Anda akan dapat membangun kustomisasi Anda di atas kode yang dihasilkan).

Untuk isolasi yang lebih besar, Anda dapat secara fisik memisahkan data baca dari data tulis. Dalam hal ini, database baca dapat menggunakan skema datanya sendiri yang dioptimalkan untuk kueri. Misalnya, ini dapat menyimpan tampilan terwujud dari data, untuk menghindari penggabungan yang kompleks atau pemetaan O/RM yang kompleks. Bahkan mungkin menggunakan jenis penyimpanan data yang berbeda. Misalnya, database tulis mungkin relasional, sedangkan database baca adalah database dokumen.

Jika database baca dan tulis terpisah digunakan, database tersebut harus tetap sinkron. Biasanya ini dicapai dengan meminta model tulis menerbitkan suatu peristiwa setiap kali memperbarui database. Untuk informasi selengkapnya tentang menggunakan peristiwa, lihat Gaya arsitektur berdasarkan peristiwa. Karena broker pesan dan database biasanya tidak dapat diikutsertakan ke dalam satu transaksi terdistribusi, mungkin ada tantangan dalam menjamin konsistensi saat memperbarui database dan menerbitkan peristiwa. Untuk informasi selengkapnya, lihat panduan tentang pemrosesan pesan idempotensi.

Arsitektur CQRS dengan penyimpanan baca dan tulis terpisah

Penyimpanan baca dapat menjadi replika baca-saja dari penyimpanan tulis, atau penyimpanan baca dan tulis dapat memiliki struktur yang berbeda sama sekali. Menggunakan beberapa replika baca-saja dapat meningkatkan performa kueri, terutama dalam skenario terdistribusi yang replika baca-sajanya terletak dekat dengan instans aplikasi.

Pemisahan penyimpanan baca dan tulis juga memungkinkan masing-masing untuk diskalakan dengan tepat agar sesuai dengan beban. Misalnya, penyimpanan baca biasanya menghadapi beban yang jauh lebih tinggi daripada penyimpanan tulis.

Beberapa penerapan CQRS menggunakan pola Sumber Peristiwa. Dengan pola ini, status aplikasi disimpan sebagai urutan peristiwa. Setiap peristiwa mewakili sekumpulan perubahan pada data. Status saat ini dibangun dengan memutar ulang peristiwa. Dalam konteks CQRS, satu keuntungan dari Sumber Peristiwa adalah bahwa peristiwa yang sama dapat digunakan untuk memberi tahu komponen lain — khususnya, untuk memberi tahu model baca. Model baca menggunakan peristiwa untuk membuat snapshot dari status saat ini, yang lebih efisien untuk kueri. Namun, Sumber Peristiwa menambah kompleksitas desain.

Keuntungan CQRS meliputi:

  • Penyekalaan independen. CQRS memungkinkan beban kerja baca dan tulis untuk diskalakan secara independen, dan dapat menghasilkan lebih sedikit pertentangan kunci.
  • Skema data yang dioptimalkan. Sisi baca dapat menggunakan skema yang dioptimalkan untuk kueri, sedangkan sisi tulis menggunakan skema yang dioptimalkan untuk pembaruan.
  • Keamanan. Lebih mudah untuk memastikan bahwa hanya entitas domain yang tepat yang melakukan penulisan pada data.
  • Pemisahan masalah. Memisahkan sisi baca dan tulis dapat menghasilkan model yang lebih mudah dipelihara dan fleksibel. Sebagian besar logika bisnis yang kompleks masuk ke model tulis. Model baca bisa relatif sederhana.
  • Kueri yang lebih sederhana. Dengan menyimpan tampilan terwujud dalam database baca, aplikasi dapat menghindari penggabungan kompleks saat melakukan kueri.

Masalah dan pertimbangan penerapan

Beberapa tantangan dalam menerapkan pola ini antara lain:

  • Kompleksitas. Ide dasar CQRS sederhana. Tapi itu bisa mengarah pada desain aplikasi yang lebih kompleks, terutama jika menyertakan pola Sumber Peristiwa.

  • Olahpesan. Meskipun CQRS tidak memerlukan olahpesan, biasanya menggunakan olahpesan untuk memproses perintah dan menerbitkan peristiwa pembaruan. Dalam hal ini, aplikasi harus menangani kegagalan pesan atau pesan duplikat. Lihat panduan tentang Antrean Prioritas untuk menangani perintah yang memiliki prioritas berbeda.

  • Konsistensi akhirnya. Jika Anda memisahkan database baca dan tulis, data yang dibaca mungkin kedaluwarsa. Penyimpanan model baca harus diperbarui untuk mencerminkan perubahan pada penyimpanan model tulis, dan mungkin sulit untuk mendeteksi saat pengguna telah mengeluarkan permintaan berdasarkan data baca yang kedaluwarsa.

Kapan menggunakan pola CQRS

Pertimbangkan CQRS untuk skenario berikut:

  • Domain kolaboratif tempat banyak pengguna mengakses data yang sama secara paralel. CQRS memungkinkan Anda untuk menentukan perintah dengan perincian yang cukup untuk meminimalkan konflik gabungan di tingkat domain, dan konflik yang muncul dapat digabungkan dengan perintah.

  • Antarmuka pengguna berbasis tugas yang penggunanya dipandu melalui proses yang kompleks sebagai serangkaian langkah atau dengan model domain yang kompleks. Model tulis memiliki tumpukan pemrosesan perintah lengkap dengan logika bisnis, validasi input, dan validasi bisnis. Model penulisan dapat memperlakukan sekumpulan objek terkait sebagai satu unit untuk perubahan data (agregat, dalam terminologi DDD) dan memastikan bahwa objek ini selalu dalam status yang konsisten. Model baca tidak memiliki logika bisnis atau tumpukan validasi, dan hanya mengembalikan DTO untuk digunakan dalam model tampilan. Model baca akhirnya konsisten dengan model tulis.

  • Skenario yang performa pembacaan datanya harus disesuaikan secara terpisah dari performa penulisan data, terutama jika jumlah pembacaan jauh lebih besar daripada jumlah penulisan. Dalam skenario ini, Anda dapat menskalakan model baca, tetapi menjalankan model tulis hanya pada beberapa instans. Sejumlah kecil contoh model tulis juga membantu meminimalkan terjadinya konflik gabungan.

  • Skenario yang satu tim pengembangnya dapat fokus pada model domain kompleks yang merupakan bagian dari model tulis, dan tim lain dapat fokus pada model baca dan antarmuka pengguna.

  • Skenario yang sistemnya diharapkan berkembang dari waktu ke waktu dan mungkin berisi beberapa versi model, atau yang aturan bisnisnya berubah secara teratur.

  • Integrasi dengan sistem lain, terutama dalam kombinasi dengan sumber peristiwa, yang kegagalan temporal dari satu subsistemnya seharusnya tidak memengaruhi ketersediaan yang lain.

Pola ini tidak direkomendasikan saat:

  • Domain atau aturan bisnisnya sederhana.

  • Antarmuka pengguna gaya CRUD sederhana dan operasi akses data sudah cukup.

Pertimbangkan untuk menerapkan CQRS ke bagian terbatas dari sistem Anda yang paling berharga.

Desain beban kerja

Arsitek harus mengevaluasi bagaimana pola CQRS dapat digunakan dalam desain beban kerja mereka untuk mengatasi tujuan dan prinsip yang tercakup dalam pilar Azure Well-Architected Framework. Contohnya:

Pilar Bagaimana pola ini mendukung tujuan pilar
Efisiensi Performa membantu beban kerja Anda memenuhi tuntutan secara efisien melalui pengoptimalan dalam penskalaan, data, kode. Pemisahan operasi baca dan tulis dalam beban kerja baca-ke-tulis yang tinggi memungkinkan performa yang ditargetkan dan pengoptimalan penskalaan untuk tujuan spesifik setiap operasi.

- PE:05 Penskalaan dan pemartisian
- Performa DATA PE:08

Seperti halnya keputusan desain apa pun, pertimbangkan tradeoff terhadap tujuan pilar lain yang mungkin diperkenalkan dengan pola ini.

Pola Sumber Peristiwa dan CQRS

Pola CQRS sering digunakan bersama dengan pola Sumber Peristiwa. Sistem berbasis CQRS menggunakan model data baca dan tulis terpisah, masing-masing disesuaikan dengan tugas yang relevan dan sering kali ditempatkan di penyimpanan terpisah secara fisik. Saat digunakan dengan pola Sumber Peristiwa, penyimpanan peristiwa adalah model tulis, dan merupakan sumber informasi resmi. Model baca dari sistem berbasis CQRS memberikan tampilan data yang terwujud, biasanya sebagai tampilan yang sangat terdenormalisasi. Tampilan ini disesuaikan dengan antarmuka dan persyaratan tampilan aplikasi, yang membantu memaksimalkan performa tampilan dan kueri.

Menggunakan aliran peristiwa sebagai penyimpanan tulis, sebagai ganti dari data aktual pada satu titik waktu, menghindari konflik pembaruan pada agregat tunggal dan memaksimalkan performa dan skalabilitas. Peristiwa dapat digunakan untuk menghasilkan tampilan data yang terwujud secara asinkron yang digunakan untuk mengisi penyimpanan baca.

Karena penyimpanan peristiwa adalah sumber informasi resmi, dimungkinkan untuk menghapus tampilan terwujud dan memutar ulang semua peristiwa masa lalu untuk membuat representasi baru dari status saat ini saat sistem berkembang, atau saat model baca harus berubah. Tampilan yang terwujud pada dasarnya adalah cache data baca-saja yang tahan lama.

Saat menggunakan CQRS yang dikombinasikan dengan pola Sumber Peristiwa, pertimbangkan hal berikut:

  • Seperti halnya sistem yang penyimpanan tulis dan bacanya terpisah, sistem yang didasarkan pada pola ini hanya akan konsisten pada akhirnya. Akan ada beberapa penundaan antara peristiwa yang dihasilkan dan penyimpanan data yang diperbarui.

  • Pola tersebut menambah kompleksitas karena kode harus dibuat untuk memulai dan menangani peristiwa, dan merakit atau memperbarui tampilan atau objek yang sesuai yang diperlukan oleh kueri atau model baca. Kompleksitas pola CQRS ketika digunakan dengan pola Sumber Peristiwa dapat membuat penerapan yang sukses menjadi lebih sulit, dan memerlukan pendekatan yang berbeda untuk mendesain sistem. Namun, sumber peristiwa dapat mempermudah untuk memodelkan domain, dan memudahkan untuk membangun kembali tampilan atau membuat yang baru karena niat dari perubahan dalam data dipertahankan.

  • Menghasilkan tampilan terwujud untuk digunakan dalam model baca atau proyeksi data dengan memutar ulang dan menangani peristiwa untuk entitas atau kumpulan entitas tertentu dapat memerlukan waktu pemrosesan dan penggunaan sumber daya yang signifikan. Hal ini terutama berlaku jika memerlukan penjumlahan atau analisis nilai selama periode yang lama, karena semua peristiwa terkait mungkin perlu diperiksa. Selesaikan ini dengan menerapkan snapshot data pada interval terjadwal, seperti jumlah total jumlah tindakan tertentu yang telah terjadi, atau status entitas saat ini.

Contoh pola CQRS

Kode berikut menunjukkan beberapa ekstrak dari contoh penerapan CQRS yang menggunakan definisi berbeda untuk model baca dan tulis. Antarmuka model tidak menentukan fitur apa pun dari penyimpanan data yang mendasarinya, dan dapat berkembang dan disesuaikan secara independen karena antarmuka ini dipisahkan.

Kode berikut menunjukkan definisi model baca.

// 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; }
  }
}

Sistem ini memungkinkan pengguna untuk menilai produk. Kode aplikasi melakukannya menggunakan perintah RateProduct yang ditunjukkan dalam kode berikut.

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; }
}

Sistem menggunakan kelas ProductsCommandHandler untuk menangani perintah yang dikirim oleh aplikasi. Klien biasanya mengirim perintah ke domain melalui sistem olahpesan seperti antrean. Penangan perintah menerima perintah ini dan memanggil metode antarmuka domain. Granularitas setiap perintah didesain untuk mengurangi kemungkinan permintaan yang bertentangan. Kode berikut menunjukkan kerangka kelas 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)
  {
    ...
  }
}

Langkah berikutnya

Pola dan panduan berikut berguna saat menerapkan pola ini:

Posting blog Martin Fowler:

  • Pola Sumber Peristiwa. Menjelaskan secara lebih rinci bagaimana Sumber Peristiwa dapat digunakan dengan pola CQRS untuk menyederhanakan tugas dalam domain yang kompleks sambil meningkatkan performa, skalabilitas, dan daya tanggap. Serta bagaimana memberikan konsistensi untuk data transaksional sambil mempertahankan riwayat dan jejak audit penuh yang dapat memungkinkan tindakan kompensasi.

  • Pola Tampilan Terwujud. Model baca dari penerapan CQRS dapat berisi tampilan material dari data model tulis, atau model baca dapat digunakan untuk menghasilkan tampilan material.

  • Presentasi tentang CQRS yang lebih baik melalui pola interaksi pengguna asinkron