كيفية تجربة CQRS / ES بسرعة في Laravel أو كتابة بنك في PHP


في الآونة الأخيرة ، في بودكاست Zinc Prod ، ناقشت أنا وأصدقائي نمط CQRS / ES وبعض ميزات تنفيذه في Elixir. لأن أستخدم Laravel في عملي ، لقد كانت خطيئة عدم الخوض في الإنترنت وليس لإيجاد كيف يمكنك ارتشاف هذا النهج في النظام البيئي لهذا الإطار.


أدعو الجميع تحت الخفض ، حاولت أن أصف الموضوع بشكل تجريدي قدر الإمكان.


بعض التعاريف


CQRS (فصل مسؤولية استعلامات الأوامر) - تخصيص عمليات القراءة والكتابة في كيانات منفصلة. على سبيل المثال ، نكتب إلى المعلم ، اقرأ من النسخة المتماثلة. CQRS. حقائق ومفاهيم خاطئة - يساعد في الحصول على معرفة شاملة بزين CQRS.
ES (Sourcing Event) - تخزين كل التغييرات في حالة الكيان أو مجموعة الكيانات.
CQRS / ES هو منهج معماري نحفظ فيه جميع أحداث تغيير الحالة لكيان ما في جدول الأحداث ونضيف مجمّعًا وجهاز عرض لهذا.
التجميع - يخزن في الذاكرة الخصائص اللازمة لاتخاذ قرارات منطق الأعمال (لتسريع الكتابة) ، ويتخذ القرارات (منطق العمل) وينشر الأحداث.
جهاز الإسقاط - يستمع إلى الأحداث ويكتب لفصل الجداول أو قواعد البيانات (لقراءة أسرع).



في المعركة


الحدث الإسقاط Laravel - مكتبة CQRS / ES لارافيل
Larabank هو مستودع مع نهج CQRS / ES. سنأخذها للتجربة.


سيخبرك تكوين المكتبة بمكان البحث وإخباره. ننظر إلى ملف event-projector.php . من الضروري وصف العمل:


  • projectors - سجل العرض.
  • reactors - سجل المفاعلات. Reactor - في هذه المكتبة ، تضيف تأثيرات جانبية إلى معالجة الأحداث ، على سبيل المثال ، في هذا المستودع ، إذا حاولت تجاوز الحد الأقصى للسحب ثلاث مرات ، يتم كتابة الحدث MoreMoneyNeeded وإرسال رسالة إلى المستخدم حول صعوباته المالية ؛
  • replay_chunk_size - حجم جزء إعادة التشغيل. إحدى ميزات ES هي القدرة على استعادة السجل من الأحداث. أعد جهاز إعداد الحدث Laravel لتسرب الذاكرة أثناء مثل هذه العملية باستخدام هذا الإعداد.

إيلاء الاهتمام للهجرة. بالإضافة إلى جداول Laravel القياسية ، لدينا


  • stored_events - جدول ES الرئيسي مع عدة أعمدة من البيانات غير المهيكلة لبيانات حدث التعريف ، نقوم بتخزين أنواع الأحداث في صف واحد. عمود مهم aggregate_uuid - يخزن معرف المستخدم الإجمالي لتلقي جميع الأحداث المتعلقة به ؛
  • accounts - جدول جهاز الإسقاط لحسابات المستخدمين ، أمر ضروري للعودة السريعة للبيانات الحالية عن حالة الرصيد ؛
  • transaction_counts - جدول لجهاز الإسقاط بعدد معاملات المستخدم ، وهو أمر ضروري للعودة السريعة لعدد المعاملات المكتملة.

والآن أقترح الدخول في الطريق مع طلب إنشاء حساب جديد.


إنشاء حساب


يصف توجيه resource القياسي AccountsController . نحن مهتمون بطريقة store


 public function store(Request $request) { $newUuid = Str::uuid(); //   ,   uuid  //      AccountAggregateRoot::retrieve($newUuid) //           ->createAccount($request->name, auth()->user()->id) //          ->persist(); return back(); } 

يرث AccountAggregateRoot مكتبة AggregateRoot . لنلقِ نظرة على الأساليب التي يطلق عليها جهاز التحكم.


 //  uuid      public static function retrieve(string $uuid): AggregateRoot { $aggregateRoot = (new static()); $aggregateRoot->aggregateUuid = $uuid; return $aggregateRoot->reconstituteFromEvents(); } public function createAccount(string $name, string $userId) { //        //  ,   recordThat,  ,    , // ..     ) $this->recordThat(new AccountCreated($name, $userId)); return $this; } 

تستدعي الطريقة persist الأسلوب storeMany النموذج المحدد في تكوين event-projector.php على أنه stored_event_model في حالتنا ، StoredEvent


 public static function storeMany(array $events, string $uuid = null): void { collect($events) ->map(function (ShouldBeStored $domainEvent) use ($uuid) { $storedEvent = static::createForEvent($domainEvent, $uuid); return [$domainEvent, $storedEvent]; }) ->eachSpread(function (ShouldBeStored $event, StoredEvent $storedEvent) { //   ,     // QueuedProjector* Projectionist::handleWithSyncProjectors($storedEvent); if (method_exists($event, 'tags')) { $tags = $event->tags(); } //         $storedEventJob = call_user_func( [config('event-projector.stored_event_job'), 'createForEvent'], $storedEvent, $tags ?? [] ); dispatch($storedEventJob->onQueue(config('event-projector.queue'))); }); } 

* QueuedProjector


يقوم ProjectPro AccountProjector و TransactionCountProjector بتنفيذ Projector وبالتالي سوف يستجيبان للأحداث بشكل متزامن مع تسجيلهم.


حسنًا ، لقد تم إنشاء حساب. أقترح النظر في كيفية قيام العميل بقراءته.


عرض الفاتورة


 //    `accounts`     id public function index() { $accounts = Account::where('user_id', Auth::user()->id)->get(); return view('accounts.index', compact('accounts')); } 

إذا قام جهاز الإسقاط للحسابات بتطبيق واجهة QueuedProjector ، فلن يرى المستخدم أي شيء حتى تتم معالجة الحدث بدوره.


أخيرًا ، سوف ندرس كيفية عمل تجديد وسحب الأموال من الحساب.


أعلى المتابعة والانسحاب


مرة أخرى ، انظر إلى AccountsController :


 //    uuid  //       //   ,     public function update(Account $account, UpdateAccountRequest $request) { $aggregateRoot = AccountAggregateRoot::retrieve($account->uuid); $request->adding() ? $aggregateRoot->addMoney($request->amount) : $aggregateRoot->subtractMoney($request->amount); $aggregateRoot->persist(); return back(); } 

النظر في AccountAggregateRoot


عند تجديد الحساب:


 public function addMoney(int $amount) { $this->recordThat(new MoneyAdded($amount)); return $this; } //    ""  recordThat // AggregateRoot*? //     apply(ShouldBeStored $event), //       'apply' . EventClassName  // ,     `MoneyAdded` protected function applyMoneyAdded(MoneyAdded $event) { $this->accountLimitHitInARow = 0; $this->balance += $event->amount; } 

* AggregateRoot


عند سحب الأموال:


 public function subtractMoney(int $amount) { if (!$this->hasSufficientFundsToSubtractAmount($amount)) { //        $this->recordThat(new AccountLimitHit()); //      ,  //   ,     //     if ($this->needsMoreMoney()) { $this->recordThat(new MoreMoneyNeeded()); } $this->persist(); throw CouldNotSubtractMoney::notEnoughFunds($amount); } $this->recordThat(new MoneySubtracted($amount)); } protected function applyMoneySubtracted(MoneySubtracted $event) { $this->balance -= $event->amount; $this->accountLimitHitInARow = 0; } 


استنتاج


حاولت وصف عملية "onboarding" في CQRS / ES في Laravel بأنها خالية من الماء قدر الإمكان. المفهوم مثير جدا للاهتمام ، ولكن ليس بدون ميزات. قبل التنفيذ ، تذكر:


  • الاتساق في نهاية المطاف.
  • من المرغوب فيه استخدام DDD في مجالات منفصلة ؛ يجب ألا تنشئ نظامًا كبيرًا بالكامل على هذا النمط ؛
  • يمكن أن تكون التغييرات في مخطط جدول الأحداث مؤلمة للغاية ؛
  • يجدر بنا أن نقترب من اختيار التفاصيل الدقيقة للأحداث ، وكلما كانت هناك أحداث ملموسة ، زادت تواجدها في الجدول وستكون هناك حاجة إلى المزيد من الموارد للعمل معها.

سأكون سعيدا لملاحظة الأخطاء.

Source: https://habr.com/ru/post/ar448000/


All Articles