如何在Laravel中快速尝试CQRS / ES或在PHP中编写银行


最近,在Zinc Prod播客中,我和我的朋友们讨论了CQRS / ES模式及其在Elixir中实现的一些功能。 因为 我在工作中使用Laravel,如果不深入Internet并没有找到如何在此框架的生态系统中使用这种方法,这是一个罪过。


我邀请大家参加会议,我试图尽可能抽象地描述这个话题。


一些定义


CQRS (命令查询职责隔离)-将读写操作分配到单独的实体中。 例如,我们写入主数据库,从副本数据库读取。 CQRS。 事实和误解 -帮助您全面了解Zen CQRS。
ES (事件源)-存储一个实体或一组实体的所有状态更改。
CQRS / ES是一种体系结构方法,其中我们将实体状态更改的所有事件保存在事件表中,并向其中添加聚合和投影仪。
聚合 -在内存中存储制定业务逻辑决策(加快编写),制定决策(业务逻辑)和发布事件所需的属性。
投影仪 -侦听事件并将其写入单独的表或数据库(以加快读取速度)。



在战斗中


Laravel事件投影仪 -Laravel的CQRS / ES库
Larabank是采用CQRS / ES方法的存储库。 我们将其试用。


库配置将告诉您在哪里看,并告诉它是什么。 我们看一下event-projector.php文件。 描述工作的必要条件:


  • projectors -套准投影仪;
  • reactors -套准反应堆。 Reactor-该库中的事件处理增加了副作用,例如,在该存储库中,如果您尝试超过提款限额的三倍,则会编写MoreMoneyNeeded事件,并向用户发送有关其财务困难的消息;
  • replay_chunk_size重播块的大小。 ES的功能之一是能够从事件中还原历史记录。 Laravel事件投影仪使用此设置为此类操作期间的内存泄漏做好了准备。

注意迁移。 除了标准的Laravel表,我们还有


  • stored_events主ES表,其中包含几列用于元事件数据的非结构化数据,我们将事件类型存储在一行中。 重要列aggregate_uuid存储aggregate_uuid的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方法在事件投影器 stored_event_model配置中指定的模型storeManystoreMany调用storeMany方法storeMany在本例中为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'))); }); } 

* 排队投影仪


投影仪AccountProjectorTransactionCountProjector实现了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; } 


结论


我试图描述在Laravel上的CQRS / ES中“入职”的过程尽可能不含水。 这个概念非常有趣,但并非没有功能。 实施之前,请记住以下几点:


  • 最终的一致性;
  • 最好在单独的域中使用DDD;您不应该完全按照这种模式构建大型系统;
  • 对事件表的架构进行更改可能会非常痛苦;
  • 负责任地,选择事件的粒度是值得的,事件越具体,表中的事件就越多,与它们一起工作将需要更多的资源。

我将很高兴注意到错误。

Source: https://habr.com/ru/post/zh-CN448000/


All Articles