Como experimentar rapidamente o CQRS / ES no Laravel ou escrever um banco em PHP


Recentemente, no podcast Zinc Prod , meus amigos e eu discutimos o padrão CQRS / ES e alguns recursos de sua implementação no Elixir. Porque Eu uso o Laravel no meu trabalho, foi um pecado não me aprofundar na Internet e não descobrir como você pode saborear essa abordagem no ecossistema dessa estrutura.


Convido todos os envolvidos, tentei descrever o tópico da maneira mais abstrata possível.


Algumas definições


CQRS (Segregação de responsabilidade de consulta de comando) - alocação de operações de leitura e gravação em entidades separadas. Por exemplo, escrevemos para o mestre, lidos na réplica. CQRS. Fatos e conceitos errôneos - Ajuda a obter um conhecimento completo do Zen CQRS.
ES (Event Sourcing) - armazenamento de todas as alterações de estado de uma entidade ou conjunto de entidades.
O CQRS / ES é uma abordagem arquitetural na qual salvamos todos os eventos da mudança de estado de uma entidade na tabela de eventos e adicionamos um agregado e um projetor a isso.
Agregado - armazena na memória as propriedades necessárias para tomar decisões de lógica de negócios (para acelerar a gravação), toma decisões (lógica de negócios) e publica eventos.
Projetor - ouve eventos e grava em tabelas ou bancos de dados separados (para uma leitura mais rápida).



Em batalha


Projetor de eventos Laravel - biblioteca CQRS / ES para Laravel
O Larabank é um repositório com uma abordagem CQRS / ES. Vamos levá-lo a julgamento.


A configuração da biblioteca lhe dirá onde procurar e o que é. Nós olhamos para o arquivo event-projector.php . Do necessário para descrever o trabalho:


  • projectors - registrar projetores;
  • reactors - registre reatores. Reator - nesta biblioteca adiciona efeitos colaterais ao processamento de eventos, por exemplo, neste repositório, se você tentar exceder o limite de retirada três vezes, o evento MoreMoneyNeeded é gravado e uma mensagem é enviada ao usuário sobre suas dificuldades financeiras;
  • replay_chunk_size - tamanho do pedaço de replay. Um dos recursos do ES é a capacidade de restaurar o histórico de eventos. O projetor de eventos do Laravel se preparou para um vazamento de memória durante essa operação usando essa configuração.

Preste atenção à migração. Além das tabelas padrão do Laravel, temos


  • stored_events - a tabela ES principal com várias colunas de dados não estruturados para dados de meta eventos, armazenamos tipos de eventos em uma linha. Coluna importante aggregate_uuid - armazena o uuid do agregado para receber todos os eventos relacionados a ele;
  • accounts - a tabela do projetor de contas de usuário, é necessária para o retorno rápido dos dados atuais sobre o estado do saldo;
  • transaction_counts - uma tabela do projetor do número de transações do usuário, necessária para o retorno rápido do número de transações concluídas.

E agora me proponho a pegar a estrada com um pedido para criar uma nova conta.


Criação de conta


O roteamento de resource padrão descreve o AccountsController . Estamos interessados ​​no método de store


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

AccountAggregateRoot herda a biblioteca AggregateRoot . Vejamos os métodos que o controlador chamou.


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

O método persist chama o método storeMany modelo especificado na configuração event-projector.php como stored_event_model no nosso caso, 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


Os projetores AccountProjector e TransactionCountProjector implementam o Projector portanto Projector eles responderão aos eventos de forma síncrona com a gravação.


Ok, uma conta foi criada. Proponho considerar como o cliente o lerá.


Exibição de fatura


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

Se o projetor de contas implementar a interface QueuedProjector , o usuário não verá nada até que o evento seja processado por sua vez.


Por fim, estudaremos como funciona a reposição e retirada de dinheiro da conta.


Recarga e retirada


Mais uma vez, veja o 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(); } 

Considere AccountAggregateRoot


ao reabastecer a conta:


 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


ao retirar fundos:


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


Conclusão


Tentei descrever o processo de “onboarding” no CQRS / ES no Laravel o mais livre de água possível. O conceito é muito interessante, mas não sem recursos. Antes de implementar, lembre-se de:


  • consistência eventual;
  • é desejável usar DDD em domínios separados, você não deve criar um sistema grande completamente nesse padrão;
  • alterações no esquema da tabela de eventos podem ser muito dolorosas;
  • De maneira responsável, vale a pena abordar a escolha da granularidade dos eventos, quanto mais eventos concretos houver, mais eles estarão na tabela e mais recursos serão necessários para trabalhar com eles.

Ficarei feliz em notar erros.

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


All Articles