はじめに
今回はよく知られたケーススタディについて話します。すべてのRails開発者は、データベース操作を行った後にメールを送信する必要に直面します。典型的には、ユーザーがサービスに登録した後に歓迎メールを送ることです。しかし、この単純なタスクは、特にActiveRecordのコールバックやデータベーストランザクション、キューが関わると、すぐに複雑になりがちです…
この問題は、Ruby on RailsにおけるActiveRecordパターンとRepositoryパターンの間で生じる混乱を説明するための前提です。
シナリオ
状況は非常に普通です:ユーザー登録はデータベースのusers
テーブルに行を作成することに対応します。ActiveRecordの用語を使うと、単純に「新しいユーザーを作成する」と言います。
そして、これが完璧です。私たちの開発者はあまり経験がないかもしれませんが、しっかり学んでおり、ActiveRecordにはafter_create
というコールバックがあり、ユーザーが作成された後にメールを送信したいということを理解しています。
また、私たちのジュニア開発者は、メール送信はキューを使って行うべきだと学んでおり、SidekiqやGood Job、Solid Queueを使っています。
あっという間に、5分で、開発者はユーザー作成後にメールを送信する設定をしました…それは動作しているように見えますが、問題があります…毎回うまくいくわけではなく、とても不安定です。時々はメールが送信されますが、時々は送信されません。
問題の分析
何がうまくいかないのかを理解するために、ジュニア開発者はここでトランザクションがどのように関わっているのかを理解する必要があります。
Railsはデータベース操作(save
)にトランザクションを使用します。つまり、ユーザー作成中にエラーが発生すると、トランザクション全体(users
テーブルへの挿入やその他の関連するアクション)はキャンセルされ、データベース内の何も変更されません。すべてが順調に進むと、Railsはトランザクションをコミットし、すべてのテーブルが同時に更新されます。
そして、ここで問題が発生します。私たちのアプリではafter_create
コールバックを使用してメールをキューに追加しますが、このコールバックはトランザクションが終了する前にトリガーされます。メールがキューに追加された時点では、トランザクションはまだ完了していません。メールジョブがトリガーされるとき、トランザクションは必ずしもコミットされておらず、ユーザーはまだデータベース内で完全に作成されていないため、ジョブの実行中にユーザーがデータベースに見つからないというエラーが発生します。
シニアからの誤ったアドバイス
もちろん、ジュニア開発者は何がうまくいかないのか理解できず、シニアに相談します。シニアはこう言います。「君は不運だ、コールバックを使っているのは悪だ、永遠に呪われるべきだ!」
ジュニアはさらに混乱します – もしコールバックを使ってはいけないなら、なぜRailsにコールバックがあるのでしょうか?
「君は責任分担を理解していない。ActiveRecordはデータベース用で、メール送信に使ってはいけない。メール送信にはサービスを使うべきだ。しかも、ActiveRecordを使ってサービスを呼ばないで、コントローラを使うべきだ。しかし、コントローラが汚れてしまうから、dry-transaction1を使うんだ。」
そして、ジュニア開発者はメールを送るために1週間を費やすことになります。
シニアは悪意があったわけではありませんが、これでは実際の問題を解決せず、状況を不必要に複雑にしています。
誤った点を整理しましょう
このケースでは、シニア開発者は間違っています。dryエコシステムや責任分担、データベースコードと外部サービス関連のコードの分離の利点を称賛することは間違っていませんが、それをジュニアに強制するのは間違いです。
シニアの間違いは、誤解にあります。ジュニアの問題はコールバックの使用に関するものではありません。実際、もしメールがジョブ内で送信されなければ、コールバックは問題にならなかったでしょう。ジョブも問題ではなく、もしジョブがトランザクションの後にトリガーされていれば、すべてうまくいったはずです。
ここでジュニアがやるべきことは、after_create
をafter_create_commit
に置き換えることです。この場合、メールはトランザクションが完了した後に送信され、問題が解決します。これにより、ジュニアはコールバックの内部動作やその順序、データベーストランザクションとの相互作用をより深く理解することができるでしょう。
シニアは、このアドバイスを最初に伝えるべきでした。もしアーキテクチャの議論をしたいのであれば、誤った、あるいは最悪の場合は詐欺的な論点を使って自分の方法を強制し、ソフトウェアアーキテクチャの決定に安心を求める代わりに、ジュニアを助けるべきでした。こうすることで、ジュニアを助けることなく、問題を回避することになります。
ただし、シニアの解決策も機能しますが、それを実装するにはコストがかかります。
ActiveRecordはRepositoryパターンではない
シニアが犯したもう一つの誤解は、ActiveRecordがRepositoryパターンではないということです。
ActiveRecordの定義に戻ると、ActiveRecordクラスはデータとビジネスロジックの両方をカプセル化することを目的としています。さて、メール送信はビジネスロジックの一部であり、このパターンではActiveRecordによってトリガーされることが完全に正当です。
Repositoryパターンでは、これは当てはまりません。Repositoryクラスの責任は、データベースとの通信のみであり、ビジネスロジックを含んではいけません。Railsでは、ActiveRecordをRepositoryパターンと一緒に使用することも可能ですが、これはActiveRecordの多くの機能(おそらくコールバックも含む)を放棄することを意味します。
しかし、これはアーキテクチャの選択です。一つのやり方しかないわけではありません。各選択肢には、それぞれの結果、利点、欠点があります。そして、もちろん、それは個人の好みにも関わる問題です。
私にとって、ActiveRecordの主な利点はコードの可読性であり、その魔法のような部分は実際には隠された複雑さです(例えば、犬に吠えさせるためにdog.bark!
と書く代わりに、DogBarker.new(AnimalFactory.create(:dog, DogAttributeValidator.validate(dog_attributes)).call
のように書かないということです)。外部サービスを呼び出す場合でも、実装の詳細は隠されています)。
しかし、この利点には代償があります。この代償を払う覚悟がない人は、別のアーキテクチャを選択することができます。しかし、他の全員に自分の決定を強制することなく、その決定を自分で確認すべきです。
ジュニアが自分のアーキテクチャの選択をし、賛否を天秤にかけて判断できるようにしましょう。
結論
すべてのアーキテクチャ選択には欠点があります。ActiveRecordにも欠点はありますが、コールバックは強力なツールであり、ジュニア開発者がそれを使用するのを躊躇させるのではなく、どのように機能し、正しく使う方法を理解し、使いこなせるように促すことは重要だと思います。
議論の中で、論拠を使い、事実に基づき、実用的で、開かれた態度を保ち、礼儀正しく接しましょう。そして、私がここでやったようなストローマン論法を使わないようにしましょう…
-
dry-transactionツールは、ビジネスステップを異なるオブジェクトにカプセル化するアプローチで、責任を分離することによりコードの構造を改善します。ただし、一貫性のために、アプローチに合わせてアプリケーション全体のコードを変換する必要があり、開発チームのトレーニングも必要となります。 ↩