بررسی عملی CQRS- بخش اول: مقدمه ای بر CQRS

در زمینه پروژه های Enterprise و همچنین بحث داغ Event Sourcing ، حتما درباره CQRS شنیده اید. در این مقاله چند قسمتی، قرار است به صورت عمیق به بررسی CQRS بپردازیم و تکنیک های لازم برای پیاده سازی یک پروژه به صورت CQRS را بررسی کنیم.



در این مقاله از Gist استفاده شده است و لود شدن کد ها ممکن است کمی زمانبر باشد

تعریف Command و Query

در تعریف، Command یک دستور است که تقاضای ایجاد و یا تغییر یک Entity را دارد و Query، یک درخواست برای گزارش گیری و یا دریافت اطلاعات یک Entity می باشد. به زبان ساده، هر درخواستی که تغییری در دیتای موجود در سیستم دهد Command و هر درخواستی که تغییری در دیتا ندهد و فقط آن را نمایش دهد Query نام دارد.


https://gist.github.com/babaktaremi/62c83bfc7b2028625c4026d3b9e70c6d


https://gist.github.com/babaktaremi/b9e916a7c0343f1278218c08d6455b22

تعریف CQS

در لغت، CQS مخفف Command Query Seperationمی باشد. این به آن معنی است که در هر درخواست، یا باید Command صورت بگیرد و یا Query.

در پروژه های روزانه، حتما به این مسئله برخورد کرده اید که متد ها و یا اکشن هایی داشتید که یا دیتا را تغییر می دادند و یا برای نمایش دیتا به دیتابیس کوئری زده می شده است. این همان مبحث CQS در ابعاد به نسبت کوچکتر می باشد.


تعریف CQRS

در لغت، CQRS به مخفف Command Query Responsibility Segregation می باشد. در CQRS علاوه بر اینکه هر درخواست باید Command یا Query باشد، مدل های درخواست نیز باید یا Command و یا Query باشند. این به آن معنی است که مدل های پروژه نیز کاملا از هم جدا هستند و هر یک نماینده یک Command و یا Query می باشند. به عنوان مثال در قطعه کد زیر دو interface داریم که هر کدام یا Query هستند و یا Command.


اینترفیس IMakeOrderCommand برای Command و IGetOrderByIdQueryHandler برای Query استفاده می شود
اینترفیس IMakeOrderCommand برای Command و IGetOrderByIdQueryHandler برای Query استفاده می شود


حال این دو اینترفیس را پیاده سازی می کنیم.


برای استفاده این دو اینترفیس را رجیستر و در OrderController تزریق می کنیم:



همانطور که دیدید. در این حالت تعداد سرویس هایی که به هر کنترلر Inject میشوند بسیار زیاد خواهد شد. برای کاهش وابستگی و ایجاد Loose Coupling بهتر است که از الگوی Mediator و کتابخانه MediatR استفاده شود.

به عنوان مثال در کد زیر با استفاده از MediatR یک Command و یک Query ساخته ایم که مدل مربوط به هرکدام از آنها با یکدیگر فرق می کند. و در نهایت تنها کافی است که اینترفیس IMediator به سیستم تزریق شود.


https://gist.github.com/babaktaremi/f25978fcd4b4600c9a597901b3b1b7b2


https://gist.github.com/babaktaremi/0ab02cf876f50b05b208ec284c1407a3

در قسمت بعدی درباره MediatR به طور کامل صحبت خواهیم کرد.


انواع CQRS

به صورت کلی، می توان CQRS را در سه سطح دسته بندی کرد:

  • در سطح کد: در این حالت تنها به جداسازی Command و Queryدر سطح کد بسنده میکنیم. در این حالت تنها سولوشن تمیز تری خواهیم داشت ولی از نظر پرفورمنس تفاوتی را شاهد نخواهیم بود.
  • در سطح دیتابیس : در این حالت به بهینه سازی دیتابیس (به خصوص برای Query ها) می پردازیم و در صورت نیاز دیتابیس Command و Query را از هم جدا میکنیم
  • در سطح کد و دیتابیس: در این حالت چون Command و Query در سطح کد جداسازی شده، یکی کردن بهینه سازی های سطح دیتابیس با کد آسان تر خواهد شد.



مقایسه CQS و CQRS

به صورت کلی ، CQS و CQRS بسیار بهم شبیهند ولی در عمل تفاوت هایی دارند. در CQS به جداسازی Command و Query در حد متد و اکشن بسنده می کند ولی در CQRS تمام مدل های Command و Query باید از یکدیگر جدا باشند. به همین علت CQRS سولوشن بسیار تمیزتری را ارائه میدهد که در آن جداسازی دیتابیس ها از یک دیگر بسیار آسانتر خواهد بود. همچنین در CQRS استفاده از الگوی Mediator بسیار حائز اهمیت است و بدون آن پیاده سازی CQRS یا بسیار سخت خواهد شد و یا در نهایت با سرویس ها و کنترلر هایی مواجه خواهیم شد که وابستگی بسیار زیادی به اجزای بسیار زیاد دارند که تغییر و توسعه آن را بسیار سخت میکند.


الگوی Mediator

در الگوی Mediator دیگر کلاس ها و سرویس ها با یکدیگر مستقیم در ارتباط نیستند بلکه درخواست خود را از طریق یک واسط می فرستند و از طریق همان واسط، پاسخ مربوطه را دریافت میکنند. این کار باعث می شود که وابستگی شدید بین دو سرویس به حداقل برسد و Loose Coupling داشته باشیم که به توسعه و نگهداری یک پروژه کمک شایانی میکند.

در CQRS استفاده از الگوی Mediator مزیت های فراوانی را به همراه دارد. هر Command و Query در سیستم با استفاده از Mediator به Handler مربوط به خود وصل میشوند و جواب درخواست نیز از طریق همین Handler تامین میشود.

الگوی Mediator باعث Loose Coupling می شود
الگوی Mediator باعث Loose Coupling می شود


چه موقع از CQRS استفاده کنیم؟

هنگامی که در یک سناریو نیاز است که دیتا مربوطه را از چند جدول لود کنیم، عمل Join بین چند جدول ممکن است زمانبر باشد و روی Performance تاثیر منفی بگذارد، پس بهتر است که دیتای مورد نیاز را در یک جدول جدا و به صورت Denormalized در بیاوریم که کوئری زدن و لود کردن آن ساده تر باشد. در اینجا استفاده از CQRS کار را بسیار راحت می کند و همچنین باعث می شود که Complexity هنگام لود کردن دیتا جدا شده و به حداقل برسد.

هنگامی که خواندن و نوشتن دیتا روی دیتابیس از مسیر های جدا صورت میگیرد ( مثلا با Store Procedure دیتا خوانده شود، و یا از روی یک Cache مانند Redis خوانده شود) استفاده از CQRS باعث Separation of Concerns خواهد شد و میتوان سرویس های مربوط به هرکدام را جداگانه توسعه داد.

هنگامی که Read Model پروژه ساده است و پیچیدگی خاصی ندارد، و یا از دیتابیس های جداگانه برای Query و Command استفاده نمی کنیم و یا میسر خواندن و نوشتن روی دیتابیس جدا نیست ( به طور مثال از یک جدول هم عملیات خواندن و هم نوشتن انجام میشود) استفاده از CQRS مزیت خاصی را به همراه ندارد و پروژه بدون استفاده از آن نیز کار خود را انجام خواهد داد.

هنگامی که مدل خواندن و نوشتن در دیتابیس جداست، CQRS میتواند پیچیدگی Command و Query را کاهش دهد
هنگامی که مدل خواندن و نوشتن در دیتابیس جداست، CQRS میتواند پیچیدگی Command و Query را کاهش دهد


مزایای استفاده از CQRS

  • باعث می شود که اپلیکیشن مقیاس پذیر باشد و توسعه آن راحت باشد. در آینده اگر لازم باشد که تمهیداتی برای Query گرفتن از دیتابیس صورت بگیرد، می توان به راحتی این تغییرات را در سطح کد انجام داد.
  • در اکثر مواقع، عمل خواندن داده بیش از سایر عملیات انجام میشود. CQRS باعث می شود که برای هر عمل Read بتوانیم تمهیدات لازم را انجام دهیم. مثلا ممکن است که در یک سناریو نیاز باشد که داده از یک SP در دیتابیس خوانده شود و در سناریو دیگر از روی Cache در Redis. پس مهم است که هر عمل Read به طور مجزا رسیدگی شود که در CQRS این کار به سادگی امکان پذیر است
  • باعث بهبود پرفرمنس میشود. حتی اگر دیتابیس های Read و Write یکی باشند ، با استفاده از CQRS می توان که روی هر کدام به صورت جداگانه Optimization انجام داد. مثلا هنگام استفاده از EF Core می توان برای Read Model داده از Second Level Cache برای cache کردن کوئری ها استفاده کرد. و یا میتوانیم از Dapper برای کوئری زدن به دیتابیس استفاده کنیم.
  • باعث ساده شدن کار با اپلیکیشن می شود. عملیات Command و Query هرکدام نیاز های خاص خود را دارند و اگر بخواهیم که از یک مدل برای هر دو استفاده کنیم، کار را سخت کرده ایم و به مدلی رسیدیم که هیچ کدام از این دو را نمیتواند به خوبی پوشش دهد. CQRS باعث می شود که برای هر کدام از Command ها و Query ها یک مدل جدا داشته باشیم و هرکدام را جدا از دیگری توسعه دهیم. پس CQRS باعث ایجاد Single Responsibility در سطح معماری پروژه می شود که هر مدل جدا از دیگر مدل ها فقط یک وظیفه دارد.


نتیجه گیری

در قسمت اول، به طور خلاصه به CQRS پرداختیم. آن را با CQS مقایسه کردیم و مزایای استفاده از CQRS اشاره کردیم. در قسمت بعدی به بررسی یکی از ابزار های بسیار محبوب و لازم برای پیاده سازی CQRS، یعنی MediatR می پردازیم و تکنیک های استفاده از این ابزار را بررسی میکنیم.

مقالات بیشتر در دات نت زوم

https://t.me/DotNetZoom