CQRSの力でレガシーなトランザクションスクリプトを読み解く、そして改善へ

:::note info
この記事は [ZOZO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/zozo) シリーズ6の13日目の記事です。
:::

# はじめに
みなさん、長い間増改築されてきたトランザクションスクリプトなレガシーコードを見たことはありますか？
特に更新系のスクリプトなんかは肥大化して可読性・変更容易性が低下しがちですよね。[^1]

本記事では、そんな肥大化した更新系のトランザクションスクリプトをCQRSの考えを参考に読み解き、改善していくための方針を紹介します。
実際に改善を行い成果を得たわけではないため、細部までは考慮できていません。一つの例として読んでいただけると幸いです。

[^1]: 当時は開発速度などの面でトランザクションスクリプトに大きなメリットがあり採用している。そのシステムが価値を生み続けてくれていることに感謝です。わざわざ書く必要もないですが一応。

# 肥大化した更新系スクリプト
本記事で対象とする肥大化した更新系スクリプトについて説明します。

トランザクションスクリプトとは、単一のリクエストに対して行うデータベースへの処理を手続き的に書いていくスタイルです。

https://bliki-ja.github.io/pofeaa/TransactionScript

規模が大きくなり過ぎないうちは特に問題もありません。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/65398ecc-f838-6b7f-58d0-3fef3079fb40.png)

「`テーブルB`を更新する時は`テーブルA`も更新しないと！」といったケースも当然出てきます。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/c79da8f3-8769-c0cb-65eb-18b5b30e1e20.png)

「更新前にこれもチェックしなきゃダメでしょ！」「あ、こっちも更新して！いや、その場合は更新しなくていいよ！」
そんなことが積み重なり爆誕するのが肥大化した更新系スクリプトです。例えばこんな感じ。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/54e1a392-722a-6d09-0986-e6e2eac281b8.png)

# CQRSの力で読み解く
では、読み解いてきましょう。いきなりですが結論から書きます。

**肥大化した更新系のトランザクションスクリプトは以下2つに分解可能な場合が多く、その境界線が分離のヒントとなる。**
 - コマンド処理(検証と実行)
 - コマンド実行イベントのサブスクライブ処理(実行)

CQRSに理解のある方であれば「それはそう」で終了かもしれません。ただ自分は多少はCQRSへの理解があったにも関わらず、意識して向き合うまでは気づくことはできませんでした。気づいてしまった今は見え方が劇的に変わっています。

それでは改めて説明していきます。

## CQRS
CQRSについて、本記事で必要な内容に焦点を当て最小限の説明をします。

https://learn.microsoft.com/ja-jp/azure/architecture/patterns/cqrs

> CQRSはコマンドクエリ責務分離を表し、データストアの読み取りと更新の操作を分離するパターンです。 


とってもざっくりいうと、コマンドとクエリでは性質が異なるのでがっつり分離しちゃいましょう。という考え方です。
ただ完全に分離するとコマンドの結果がクエリ結果に反映されなくなってしまうのでイベントを介してこれらを非同期に連携します。図で表すと以下の通りです[^2]。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/c89aa621-d230-cb20-3268-d415fae1b301.png)

[^2]: [実践ドメイン駆動設計](https://www.amazon.co.jp/dp/B00UX9VJGW)のP135の図4-6を参考に作成しました

本記事では、**CQRSの考え方においてはコマンドモデルの更新とクエリモデルの更新が明確に分かれている**[^3]という点が重要なのでこれだけ覚えて先に進みます。

[^3]: このような構成を取らず単にコマンドとクエリのモデルを分けるだけでもCQRSと呼ぶ場合もあるようです。

CQRSについて詳しく知りたい方は書籍や別の記事に目を通していただけると幸いです。
個人的には[かとじゅんさん](https://twitter.com/j5ik2o)の発信する情報に色々と目を通して勉強しました。
 
## 改めて肥大化した更新系スクリプトを見てみる
今回題材としたレガシーシステムはCQRSに沿った構成ではなく、同一のRDBをクエリとコマンド双方で利用しています。
つまり、**更新系のスクリプトではコマンドモデルの更新とクエリモデルの更新が同時に行われているのです。**

この考え方で先ほど示した図を分解するとこうなります。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/6cfb9384-3656-30fe-3184-57e2d2e53816.png)

今回は説明のために用意した例のため、極端に綺麗に分解できてしまっています。
ただ、一見すると気圧されてしまうような実際の現場の肥大化したスクリプトを眺めてみても、**更新の実行を判断する重要なビジネスロジックはスクリプトの前半にあり、後半に登場する大量のSQLたちは実際はクエリのためのデータ更新ばかり、あるいは順番を並び替えたらそうなる**という場合が意外と多いと感じています。

# そして改善へ
肥大化したスクリプトを読み解くことができたので、一つの改善方針を説明してきます。[^4]

方針は以下の通りです。
- 既存のシステムはクエリシステムとして残す
- コマンドに責務を持つシステムを前段に作成する
- イベントを利用して両者を非同期で連携する

 図で表すとこうなります。
 ![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330036/38cf4705-809c-fc53-2908-0a100fa729c3.png)

これにより、既存のシステムも活かしつつ、ビジネス上重要なロジックが集まるコマンドに関するシステムを新たに作り上げることが(理論的には)可能です。

ただし、変更前にはRDBのACIDなトランザクションを利用してコマンドモデルの更新と同期的に行われていたクエリモデルの更新が非同期な形に変更となります。このような結果整合性が許容できるのか？が一番の問題になると思います。 

[^4]: 具体的な課題を整理しないとこの方針が正解か？は判断できないと思います。「場合によってはこのアプローチもアリかもな」くらいの気持ちで読んでいただけると。

# おわりに
本記事では、レガシーな更新系のトランザクションスクリプトをCQRSの考え方を参考に読み解き、改善していくための方針についても書きました。

自分自身、この記事を書くことでレガシーコードへの解像度がより一層高くなったと感じることができました。
紹介した方針に関しては、大きなメリットも感じています。ただし、整合性の問題や実際の運用負荷の問題を考えると非常にコストが高いとも感じています。

アーキテクチャについて考えれば考えるほど、「嗚呼、トレードオフ・・・」であり全てがよくなる選択肢など存在しないということが実感できます。加えて投入できるリソースも限られているので、痛みを払ってでも改善が必要な箇所を選択し、集中して取り組んでいきます。