در این قسمت به سراغ پترن CQRS می رویم، CQRS مخفف Command and Query Responsibility Segregation است، الگویی که عملیات خواندن و به روز رسانی را برای ذخیره دادهها جدا می کند. پیاده سازی CQRS در برنامه شما می تواند performance، scalability و امنیت آن را به حداکثر برساند. انعطافپذیری ایجاد شده توسط CQRS به سیستم اجازه میدهد در طول زمان بهتر تکامل یابد و از ایجاد merge conflicts توسط بهروزرسانیها جلوگیری میکند.
طرح صورت مسئله:
در معماریهای قدیمیتر نرم افزار، از data model یکسانی برای query و به روز رسانی پایگاه داده استفاده می شود. این روش خیلی ساده بوده و برای عملیات اولیه و معمولی CRUD به خوبی کار می کند. با این حال، در برنامه های پیچیده تر، این روش خوب نیست. به عنوان مثال: در سمت خواندن پایگاه داده، برنامه ممکن است queryهای مختلفی را انجام دهد و data transfer objects (DTO) را با اشکال مختلف برگرداند. Object mapping می تواند پیچیده شود. در سمت نوشتن، مدل ممکن است validation و business logic پیچیدهای را پیاده سازی کند. در نتیجه؛ با یک مدل بیش از حد پیچیده روبرو میشویم که بیش از حد کار می کند و به نوعی تحت فشار است. حجم عملیات خواندن و نوشتن نامتقارن و نابرابر است و به جهت مشکلات performance در این موارد نیاز به scale برنامه بوجود می آید.
راه حل:
به طور کلی CQRS خواندن و نوشتن را در مدلهای مختلف جدا میکند و از دستورات برای بهروزرسانی دادهها و پرس و جو برای خواندن دادهها استفاده میکند.
سپس می توان مدل ها را جدا کرد، همانطور که در نمودار زیر نشان داده شده است.
داشتن مدل های پرس و جو و به روز رسانی جداگانه، طراحی و پیاده سازی را ساده می کند. با این حال، یک نقطه ضعف این است که کد CQRS نمی تواند به طور خودکار از یک طرح پایگاه داده با استفاده از مکانیسم هایی مانند ابزارهای O/RM تولید شود.
برای جداسازی بیشتر، می توانید داده های خوانده شده را از داده های نوشتن به صورت فیزیکی جدا کنید. در آن صورت، پایگاه داده خوانده شده می تواند از طرح داده های خود استفاده کند که برای پرس و جوها بهینه شده است. به عنوان مثال، می تواند یک نمای مادی از داده ها را ذخیره کند تا از اتصالات پیچیده یا O/RM mapping پیچیده جلوگیری کند. حتی ممکن است از نوع دیگری از ذخیره داده استفاده کند. به عنوان مثال، پایگاه داده نوشتن ممکن است relational باشد، در حالی که پایگاه داده خوانده شده یک پایگاه document database است.
اگر از پایگاههای داده خواندن و نوشتن جداگانه استفاده میشود، باید آنها را sync نگه داشت. این حالت معمولاً با publish شدن یک event توسط مدل نوشتن زمانی که پایگاه داده را به روز شود، انجام می شود. برای اطلاعات بیشتر در مورد استفاده از رویدادها، به سبک معماری Event-driven architecture style مراجعه کنید. به روز رسانی پایگاه داده و publish شدن event باید در یک تراکنش (transaction) انجام شود.
ذخیره داده های خواندن میتواند یک (replica)کپی read-only از ذخیره داده های نوشتن باشد، یا ذخیره داده های خواندن و نوشتن میتوانند ساختار متفاوتی داشته باشند. استفاده از چند replica به صورت read-only می تواند performance query را افزایش دهد، به خصوص در سناریوهای distributed شده که در آن replica های فقط خواندنی نزدیک به برنامه در حال اجرا قرار دارند.
جداسازی ذخیره دادههای خواندن و نوشتن اجازه میدهد تا هر کدام بهطور مناسب برای مطابقت با load برنامه و دستورات، scale شوند. به عنوان مثال، ذخیره داده های خواندن معمولاً با load بسیار بالاتری نسبت به ذخیره دادههای نوشتن مواجه می شوند.
برخی از پیاده سازی های CQRS از الگوی Event Sourcing استفاده می کنند. با این الگو، وضعیت برنامه به عنوان یک دنبالهای از eventها ذخیره می شود. هر event نشان دهنده مجموعه ای از تغییرات در داده ها است. وضعیت فعلی با پخش مجدد رویدادها ساخته می شود. در موضوع CQRS، یکی از مزایای Event Sourcing این است که از همان رویدادها می توان برای اطلاع رسانی (notify) به اجزای دیگر استفاده کرد - به ویژه برای اطلاع رسانی(notify) به مدل خوانده شده(read model). مدل خواندن از eventها برای ایجاد یک snapshot از وضعیت فعلی استفاده می کند که برای پرس و جوها کارآمدتر است. با این حال، Event Sourcing پیچیدگی را به طراحی اضافه می کند.
مزایای CQRS عبارتند از:
برخی از چالش های اجرای این الگو عبارتند از:
چه زمانی از این الگو استفاده کنیم؟
برای CQRS سناریوهای زیر را در نظر بگیرید:
این الگو در موارد زیر توصیه نمی شود:
استفاده از CQRS را در بخش های محدودی از سیستم خود در نظر بگیرید که در آن بیشترین ارزشو حساسیت را دارد.
الگوی CQRS اغلب همراه با الگوی Event Sourcing استفاده می شود. سیستمهای مبتنی بر CQRS از مدلهای داده خواندن و نوشتن جداگانه استفاده میکنند که هر کدام برای وظایف مربوطه طراحی شدهاند و اغلب در ذخیره سازی فیزیکی مجزا قرار دارند. هنگامی که با الگوی Event Sourcing استفاده می شود، ذخیره رویدادها write model است و منبع official source دادهها است. read model یک سیستم مبتنی بر CQRS که materialized views دادهها معمولاً به صورت viewsهای بسیار denormalized شده ارائه میکند. این viewها بر اساس واسط ها و الزامات نمایش برنامه طراحی شده اند که به حداکثر رساندن performance نمایش و query کمک می کند.
استفاده از stream of events بهعنوان ذخیرهسازی نوشتن بهجای دادههای واقعی در یک نقطه خاصی از زمان قطعا از update conflicts در یک aggregate اجتناب میکند و performance و scalability را به حداکثر میرساند. از eventها می توان برای تولید ناهمزمان materialized views داده هایی که برای پر کردن ذخیرهسازی خوانده استفاده می شود استفاده کرد.
از آنجایی که ذخیرهسازی رویداد (event store) منبع رسمی اطلاعات دادههاست، میتوان materialized views را حذف کرد و همه رویدادهای گذشته را مجدداً پخش کرد تا زمانی که سیستم تکامل مییابد یا زمانی که مدل read model باید تغییر کند یا نمایش جدیدی از وضعیت فعلی ایجاد شود. materialized views یافته در واقع یک حافظه cache پنهان read-only از داده ها هستند.
هنگام استفاده از CQRS همراه با الگوی Event Sourcing، موارد زیر را در نظر بگیرید:
کد زیر چکیده ای از پیاده سازی CQRS را نشان می دهد که از تعاریف مختلفی برای مدل های خواندن و نوشتن استفاده می کند. رابطهای مدل هیچ ویژگی ذخیرهسازی دادههای زیربنایی را تعیین نمیکنند و میتوانند به طور مستقل توسعه یافته و تنظیم شوند زیرا این رابطها از هم جدا هستند.
کد زیر تعریف مدل خوانده شده را نشان می دهد.
// 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; }
}
}
این سیستم به کاربران اجازه می دهد تا محصولات را رتبه بندی کنند. کد برنامه این کار را با استفاده از دستور RateProduct نشان داده شده در کد زیر انجام می دهد.
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; }
}
این سیستم از کلاس ProductsCommandHandler برای کنترل دستورات ارسال شده توسط برنامه استفاده می کند. کلاینت ها معمولاً دستورات را از طریق یک سیستم messaging مانند queue به دامنه ارسال می کنند. کنترل کننده فرمان این دستورات را می پذیرد و متدهای رابط دامنه را فراخوانی می کند. جزئیات هر فرمان به گونه ای طراحی شده است که شانس درخواست های conflict را کاهش دهد. کد زیر یک طرح کلی از کلاس 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)
{
...
}
}
الگوها و راهنمایی های زیر هنگام اجرای این الگو مفید هستند