Cara cepat mencoba CQRS / ES di Laravel atau menulis bank dalam PHP


Baru-baru ini, di podcast Zinc Prod , saya dan teman-teman membahas pola CQRS / ES dan beberapa fitur penerapannya di Elixir. Karena Saya menggunakan Laravel dalam pekerjaan saya, adalah dosa untuk tidak menyelidiki Internet dan tidak menemukan bagaimana Anda dapat menyesap pendekatan ini dalam ekosistem kerangka kerja ini.


Saya mengundang semua orang di bawah potongan, saya mencoba menggambarkan topik seabstrak mungkin.


Beberapa definisi


CQRS (Segregation Command Query Responsibility) - alokasi operasi baca dan tulis menjadi entitas yang terpisah. Sebagai contoh, kami menulis kepada master, membaca dari replika. CQRS. Fakta dan Kesalahpahaman - Membantu mendapatkan pengetahuan menyeluruh tentang Zen CQRS.
ES (Event Sourcing) - penyimpanan semua perubahan status suatu entitas atau kumpulan entitas.
CQRS / ES adalah pendekatan arsitektur di mana kami menyimpan semua peristiwa perubahan status entitas dalam tabel acara dan menambahkan agregat dan proyektor untuk ini.
Agregat - menyimpan dalam memori properti yang diperlukan untuk membuat keputusan logika bisnis (untuk mempercepat penulisan), membuat keputusan (logika bisnis) dan menerbitkan acara.
Proyektor - mendengarkan acara dan menulis ke tabel atau basis data yang terpisah (untuk membaca lebih cepat).



Dalam pertempuran


Proyektor acara Laravel - perpustakaan CQRS / ES untuk Laravel
Larabank adalah repositori dengan pendekatan CQRS / ES. Kami akan membawanya untuk diadili.


Konfigurasi pustaka akan memberi tahu Anda ke mana harus mencari dan memberi tahu apa itu. Kami melihat file event-projector.php . Dari yang diperlukan untuk menggambarkan pekerjaan:


  • projectors - daftar proyektor;
  • reactors - daftar reaktor. Reactor - di perpustakaan ini menambahkan efek samping ke pemrosesan acara, misalnya, dalam repositori ini, jika Anda mencoba untuk melebihi batas penarikan tiga kali, acara MoreMoneyNeeded ditulis dan pesan dikirim kepada pengguna tentang kesulitan keuangannya;
  • replay_chunk_size - ukuran potongan replay. Salah satu fitur ES adalah kemampuan untuk memulihkan sejarah dari berbagai peristiwa. Proyektor acara Laravel disiapkan untuk kebocoran memori selama operasi semacam itu menggunakan pengaturan ini.

Perhatikan migrasi. Selain tabel Laravel standar, kami memiliki


  • stored_events - tabel ES utama dengan beberapa kolom data tidak terstruktur untuk data acara meta, kami menyimpan jenis acara dalam satu baris. Important column aggregate_uuid - menyimpan uuid dari agregat untuk menerima semua peristiwa yang terkait dengannya;
  • accounts - tabel proyektor akun pengguna, diperlukan untuk pengembalian cepat data saat ini pada keadaan keseimbangan;
  • transaction_counts - tabel proyektor dari jumlah transaksi pengguna, yang diperlukan untuk pengembalian cepat dari jumlah transaksi yang diselesaikan.

Dan sekarang saya mengusulkan untuk berangkat dengan permintaan untuk membuat akun baru.


Pembuatan akun


Perutean resource standar menjelaskan AccountsController . Kami tertarik dengan metode store


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

AccountAggregateRoot mewarisi pustaka AggregateRoot . Mari kita lihat metode yang disebut pengendali.


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

Metode persist memanggil metode storeMany model yang ditentukan dalam konfigurasi event-projector.php sebagai stored_event_model dalam kasus kami, 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'))); }); } 

* Proyek Antrian


Proyektor AccountProjector dan TransactionCountProjector mengimplementasikan Projector oleh karena itu mereka akan merespons peristiwa secara serempak dengan rekaman mereka.


Oke, sebuah akun telah dibuat. Saya mengusulkan untuk mempertimbangkan bagaimana klien akan membacanya.


Tampilan faktur


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

Jika proyektor akun mengimplementasikan antarmuka QueuedProjector , maka pengguna tidak akan melihat apa-apa sampai acara diproses secara bergantian.


Akhirnya, kita akan mempelajari bagaimana pengisian dan penarikan uang dari akun bekerja.


Isi ulang dan penarikan


Sekali lagi, lihat 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(); } 

Pertimbangkan AccountAggregateRoot


saat mengisi akun:


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

* AgregatRoot


saat menarik dana:


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


Kesimpulan


Saya mencoba menggambarkan proses "onboarding" di CQRS / ES di Laravel sebebas mungkin dalam air. Konsepnya sangat menarik, tetapi bukan tanpa fitur. Sebelum menerapkan, ingat tentang:


  • konsistensi akhirnya;
  • diinginkan untuk menggunakan DDD di domain terpisah, Anda tidak harus membuat sistem besar pada pola ini;
  • perubahan skema tabel acara bisa sangat menyakitkan;
  • Secara bertanggung jawab, perlu mendekati pilihan granularitas peristiwa, semakin banyak kejadian konkret, semakin mereka akan berada dalam tabel dan semakin banyak sumber daya yang dibutuhkan untuk bekerja dengannya.

Saya akan senang melihat kesalahan.

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


All Articles