yoskhdia’s diary

DDDとかプログラミングとかアーキテクチャとか雑多に

そのトランザクションは果たして本当にトランザクションなのだろうか?

DDDネタです。 DDD Community-JPのDiscordで「複数の集約(Aggregate)をまたいで整合性をどう担保するのが良いのか?」という話がされていました。 この話を読んでいて、

yoskhdia.hatenablog.com

でもサラッと触れた「トランザクション」をもう少し掘ってみようかなと思い立ったので書いてみるエントリです。

先の記事では次のように書きました。

実業務、ドメインを見れば、本当にまとめて処理しなきゃいけないものは、結構少ないはずです。 働く人たちは、どういう会話をしているのか、仕事の単位は何なのか、によってトランザクションを設計することが、メッセージングシステムを考えるうえで有用だと考えています。

トランザクション」という言葉は、開発者にとっていくつかの意味を持ちます。 ここでは、

の2つに絞って考えます。

DBトランザクション

これはわかりやすいですよね。

BEGIN TRANSACTION;

-- ...

COMMIT; -- or ROLLBACK;

トランザクションです*1。 データの整合性を守らなければならない範囲を定義します。

業務トランザクション

Transactionを辞書でひくと処理や業務、取引という言葉が出てきます。

ここでは、人が行う業務プロセスのなかで守らなければならない ひとつの仕事の単位 と捉えています*2。 たとえば、「伝票を受け取って、誤りがないかチェックする」のようなものです。

ビジネスの現場で「トランザクション」というとき、多くの場合は業務トランザクションです。 つまり、ドメインエキスパートが話すトランザクションは、業務の区切りです。

その「トランザクション」の範囲は適切なのだろうか

話を現場に戻したとき、たとえば「伝票を受け取って、誤りがないかチェックする」というケースでは次のような選択肢があります。

  • ひとつでも誤りがあったら伝票をまるっと突き返す
  • 誤りのあった部分だけ訂正を求めて、良かったものは次に回す

「伝票を受け取って、誤りがないかチェックする」という作業をシステムに置き換えるとき、前者ばかりで設計していたりしないでしょうか。 これまでは前者の方式であったとしても、システム化にあたって後者にした方が業務効率が上がる可能性はないでしょうか。

と、このように、伝票チェック処理はまるっとDBトランザクションかけておきましょうーに対して、本当にそのトランザクションはより良いトランザクションなの?と問いかけることが、ドメインを探求するということなのかなぁと思います。

開発者は、DBトランザクションと業務トランザクションを近づける努力を行いますが、先にあるのは業務トランザクションの方です。 一方で、この処理とこの処理を別のDBトランザクションにすると、整合性が保てない!というケースもあるかもしれません。 このときは、業務トランザクションにフィードバックを行います。 その業務トランザクションは、実際には整合性を担保できない境界を破壊したトランザクションである可能性があるからです。

そして集約へ

「集約(Aggregate)はトランザクション整合性の境界と同義」というのはIDDD本の p.340 に記述がありますが、同 p.340-344 には、さらに次のような記述があります。

トランザクションの分析をしてからでないと、集約の設計の良し悪しを正しく判断することはできない

複数の集約をまたいで整合性を担保する必要がある、というのは、その集約は実は1つの集約である可能性があります。 また、それだとあまりに巨大な集約となってしまうという場合は、トランザクションの分析が足りない可能性があります。

業務プロセスの分析・設計に、技術的な視点を加えることができるのは開発者であるので、相互に揺さぶりをかけていくことが肝要です。

私はどういう風に取り組んだか

DDD本 p.124 には次のような記述があります。

維持すべき不変条件には、個々のオブジェクトに適用されるものだけでなく、密接に関連するオブジェクトのグループに適用されるものもある。 だが、慎重にロックしすぎると、今度は複数のユーザが指針もなく相互に干渉し合い、システムが使いものにならなくなる。 (中略) データの永続ストレージを持つシステムには、データを更新するトランザクションのためのスコープと、データの一貫性を維持する(つまり、不変条件を維持する)方法がなければならない。

必要な要件が2つあることは示していますが、それが同じであるとは書かれていません*3

ということで、DBへのRead/Writeとドメインオブジェクトへの操作は切り離しました。 具体的には、DBとやり取りを行うのはユースケース層(アプリケーションサービス層)に限定し、ドメイン層のなかでは永続化については考えず、不変条件にのみフォーカスしました。さらに、Read/Writeは処理をまとめて、例えばReadブロック→処理ブロック→Writeブロックのようにブロック単位で分離しています。先にロジックによる検証がされてから、永続化に回すという感じです。

これによって、永続化時点で発生する問題はDBが途中でダウンしたなどの技術的な問題のみになります。 そうなれば、リトライするなり冗長化しておくなり技術的な解決策をとれば良いです。 私の場合は、多少一部のデータだけがコミットされた状態であっても、誤差として許容できる、いずれ正しくなるという方向で設計しました。 その点では、再実行耐性*4や並列実行耐性*5みたいな観点は大切です。

もちろんこれは一例で、たまたま取り組んだドメインではコレが適していそうだった、ということです。

まとめ

DBトランザクションと業務トランザクションを近づける努力は開発者が行います。 トランザクションはたとえば次のような観点で見出されるものです。

  • どういう会話をしているのか
  • 仕事の単位は何なのか

実際の現場ではどのようにアクションが取られている(もしくは、取るべき)かに着目します。 システムを設計するなかで、より良いトランザクションの定義を見つけたら、業務サイドにもフィードバックを行うことでドメインの探求が進むと考えられます。 技術によってビジネスを後押しするというのは、こういうことなのかなぁと思います。

DDD本の p.124 から引用します。

この問題は、データベーストランザクションにおける技術的な難問であるように見えるが、その根源はモデルにある。つまり、境界が定義されていないことが問題なのだ。 解決策をモデルから導き出すことによって、モデルは理解しやすくなり、設計は伝達しやすくなる。 モデルが改訂されると、その改訂によって実装を変更するよう導かれるのだ。

*1:この記事を読んでいる人は開発者だ、という暗黙のコンテキストにたっています。

*2:プロセスは仕事の連続、と定義しています

*3:ざっと目を通して書いてなさそうでしたが、もしどこかに記述があったら教えてください

*4:同じ処理が2回実行されても結果に影響がでないようにする

*5:同じ処理が2つ以上のプロセスで並行して実行されても結果に影響がでないようにする