Recently, in a podcast on Zinc Products , my colleagues and I discussed the CQRS / ES pattern and some features of its implementation in Elixir. Because I use Laravel in my work, it was a sin not to rummage through the Internet and not to find out how to approach this approach in the ecosystem of this framework.
I invite everyone under the cat, tried to describe the topic as briefly as possible.
CQRS (Command Query Responsibility Segregation) - selection of read and write operations into separate entities. For example, we write to the master, we read from the replica. CQRS. Facts and Fallacies - will help to thoroughly learn Zen CQRS.
ES (Event Sourcing) - storing all state changes of an entity or set of entities.
CQRS / ES is an architectural approach where we save all events of a state change of an entity in the event table and add an aggregate and a projector to it.
Aggregate - stores in memory the properties necessary for making decisions by the business of logic (to speed up writing), makes decisions (business logic) and publishes events.
Projector - listens to events and writes in separate tables or bases (to speed up reading).
Laravel event projector - CQRS / ES library for Laravel
Larabank is a repository with a CQRS / ES implemented approach. And take it on trial.
The library configuration will tell you where to look and tell you what it is. See the event-projector.php file. From the necessary to describe the work:
projectors
- register projectors;reactors
- register the reactors. Reactor - in this library adds side effects to event handling, for example, in this repository, if you try to exceed the withdrawal limit three times, the MoreMoneyNeeded event is written and a letter is sent to the user about its financial difficulties;replay_chunk_size
- size of the chunk replay. One of the features of ES is the ability to restore the history of events. Laravel event projector prepared for a memory leak during such an operation using this setting.Pay attention to migration. In addition to standard Laravel tables, we have
stored_events
is the main ES table with several columns of unstructured data for event meta data, a line storing event types. The important column aggregate_uuid
- stores the uuid of the aggregate, to receive all the events related to it;accounts
- the table of the projector user accounts, necessary for quick return of current data on the state of balance;transaction_counts
- the projector's table of the number of user transactions, necessary for the quick return of the number of completed transactions.And now I propose to go along with the request to create a new account.
Standard resource
routing describes Accounts Controls . We are interested in the store
method
public function store(Request $request) { $newUuid = Str::uuid(); // , uuid // AccountAggregateRoot::retrieve($newUuid) // ->createAccount($request->name, auth()->user()->id) // ->persist(); return back(); }
AccountAggregateRoot inherits the AggregateRoot library. Let's see the methods that the controller called.
// 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; }
The persist
method calls the storeMany
method storeMany
the model specified in the configuration event-projector.php as stored_event_model
in our case 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'))); }); }
The AccountProjector and TransactionCountProjector projectors implement the Projector
so that they will react to events synchronously with their recording.
Ok, account created. I propose to consider how the client will read it.
// `accounts` id public function index() { $accounts = Account::where('user_id', Auth::user()->id)->get(); return view('accounts.index', compact('accounts')); }
If the invoice projector implements the QueuedProjector interface, then the user will not see anything until the event is processed in turn.
Finally, we will study how the deposit and withdrawal of money works.
Again, look at the AccountsController controller:
// 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(); }
Consider AccountAggregateRoot
when replenishing an account:
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; }
when withdrawing funds:
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; }
I tried as much as possible without water to describe the onboarding process in CQRS / ES on Laravel. The concept is very interesting, but not without features. Before embedding remember:
I will be glad to see the errors.
Source: https://habr.com/ru/post/448000/
All Articles