Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby on Railsのトランザクションをマスターする:押さえておきたい5つの基本設計プラクティス

Rails開発者が直面するデータベース問題の中でも、データ整合性(データインテグリティ)の問題は最も一般的なものの一つです。適切なバリデーションを行うだけでなく、正しく設計されたトランザクションブロックを使えば、データが部分的に作成・更新されてしまう事態を防ぐことができます。

しかし一方で、トランザクションは設計を誤るとアプリケーションに悪影響を及ぼし、最悪の場合データベース全体をダウンさせてしまうこともあります。

本記事では、トランザクションを扱う際の良いプラクティスを紹介します。どれもシンプルなTipsですが、トランザクションを堅牢で読みやすく、比較的安全なものにする助けになるはずです。

それでは始めましょう!

1. 可能な限り「!」付きメソッド(bangメソッド)を使う

Railsでは、メソッド名に!が付いたバージョンを使うことで、何か問題が起きたときに必ず例外が発生することを保証できます。

例えば、#saveメソッドにはsave!というバージョンも存在します。エラーを発生させたくない場合、コントローラーでは次のように書きたくなるかもしれません。

def create
  @user = User.new(user_params)
  if @user.save
    redirect_to @user
  else
    render :new
  end
end

しかし、このアプローチはトランザクション内ではうまく機能しません。saveを使うと、エラー発生時に処理をロールバックできないからです。だからこそ、トランザクション内では!付きのメソッドを使うことが非常に重要になります。

ActiveRecord::Base.transaction do
  user.save!
  membership = Membership.new(user: user, group: group)
  membership.save # ここで失敗してもロールバックされない!
end

上記のコードでは、membershipレコードが作成されなくてもトランザクションは成功とみなされ、結果としてデータベースには不整合のあるデータ構造が残ってしまいます。

次のように!付きメソッドを使えば、ActiveRecord::RecordNotSavedエラーが発生した時点でトランザクションがロールバックされます。

ActiveRecord::Base.transaction do
  user.save!
  membership = Membership.new(user: user, group: group)
  membership.save! # エラー発生時に自動的にロールバックされる
end

2. トランザクション内のエラーを適切に処理する

トランザクションにおけるエラー処理には、守るべきルールがいくつかあります。これらのルールに従えば、他の開発者を混乱させたり、デバッグが難しい奇妙な挙動を引き起こしたりしない、読みやすく安定して動作するコードになります。

ActiveRecord::StatementInvalidをrescueしない

ActiveRecord::StatementInvalidは、データベースレベルで何か問題が起きたときに発生する特別なエラーです。このエラーは決してrescueしてはいけません。データベースクエリに問題が起きたときは、常に明示的に通知されるべきだからです。

以下のようなコードは避けましょう。

begin
  ActiveRecord::Base.transaction do
    # データベース操作
  end
rescue ActiveRecord::StatementInvalid => e
  # 悪い例:DBレベルの重大なエラーを握りつぶしてしまう
  logger.error('something went wrong')
end

rescueは適切な階層で行う

次のようにトランザクションブロックの内側でrescueすると、エラーを捕捉してしまいます。

ActiveRecord::Base.transaction do
  begin
    user.save!
    order.create!
  rescue => e
    # 悪い例:ここでエラーを捕捉するとロールバックされない
    logger.error(e.message)
  end
end

エラーを捕捉してしまうと、トランザクションはロールバックされません。エラーをそのまま発生させ、トランザクションブロックの外側で捕捉しましょう。

begin
  ActiveRecord::Base.transaction do
    user.save!
    order.create!
  end
rescue => e
  # 良い例:ロールバックされた上でエラーを捕捉できる
  logger.error(e.message)
end

このアプローチなら、エラー発生時にトランザクションがロールバックされ、かつエラーを捕捉できます。トランザクションの挙動を上書きすることなく、トランザクション内で発生したエラーを捕捉する正しい方法です。

汎用的なエラーを捕捉しない

StandardErrorArgumentErrorといった汎用的なエラーを捕捉するのは避けるべきです。これは読みやすくテストしやすいコードのための一般的なルールですが、あえて取り上げる価値があります。

これらのエラーを捕捉すると、コードの他の場所で発生したエラーまで巻き込んでしまうため、デバッグが難しくなります。rescueした場所とは必ずしも関係ない、アプリケーションの深刻な問題を見逃してしまう可能性もあるのです。

ActiveRecordのデフォルトのロールバックエラーを賢く使う

ActiveRecordには、トランザクション内でサイレントにロールバックを行うために使える特別なエラークラスが用意されています。ActiveRecord::Rollbackをraiseするとトランザクションがロールバックされますが、他のエラーと異なり、外側へはエラーが伝播しません。この挙動を理解した上で、賢く活用しましょう。

ActiveRecord::Base.transaction do
  unless stock.available?
    raise ActiveRecord::Rollback # 外側にはエラーが伝わらない
  end
  order.create!
end
# ここではエラーが発生しない

3. トランザクションを使うべきでない場面を知る

何事にも言えることですが、コード内でトランザクションを使いすぎてはいけません。よくある間違いの一つは、たった一つのクエリだけをトランザクションで包むことです。これは意味がありません。そのクエリが失敗しても、ロールバックすべきものが何もないからです。

もう一つのよくある間違いは、データベース呼び出しと無関係なコードをトランザクションで包むことです。このような書き方は避けるべきです。ブロック内のコードが実行を終えるまで、トランザクションはデータベース接続を保持し続けるからです。可能であれば、ブロック内のコードはデータベースへの呼び出しのみに限定しましょう。

# 悪い例:重い外部API呼び出しをトランザクション内で実行している
ActiveRecord::Base.transaction do
  result = ExternalApi.fetch_data # 接続を長時間占有してしまう
  user.update!(data: result)
end

# 良い例:API呼び出しはトランザクションの外で行う
result = ExternalApi.fetch_data
ActiveRecord::Base.transaction do
  user.update!(data: result)
end

4. トランザクションのデメリットを理解する

トランザクションはデータベース内のデータ整合性を保つのに役立ちますが、そのデメリットについても認識しておく必要があります。例えば、トランザクションブロックで包まれたクエリは、単一のクエリよりも多くのDBリソースを消費します。

また、トランザクションを使うことのもう一つの欠点は、コードが複雑になりがちな点です。誤った使い方をすると、コードの可読性を損なう原因にもなります。

5. 適切なコンテキストでトランザクションブロックを使う

transactionメソッドは、ActiveRecordクラスを継承するクラスであればどこでも使えます。ただし、どのバージョンを使うかがまったく無意味というわけではありません。機能的な観点では大きな違いがないとしても、コードの可読性を保つという観点では重要です。

transactionメソッドの使い方には、主に3つのパターンがあります。

ActiveRecord::Base.transaction do
  # 複数モデルを横断して扱う場合
end

User.transaction do
  # 特定のモデルに関連する処理
end

@user.transaction do
  # インスタンスに対する処理
end

複数のモデルを扱い、ブロック内でクラスとインスタンスメソッドの呼び出しが混在する場合は、ActiveRecord::Base.transactionを使いましょう。

ActiveRecord::Base.transaction do
  user.save!
  group.memberships.create!(member: member)
  invoice.create!
end

特定のモデルに属するコードが中心であれば、クラスに対してtransactionメソッドを呼び出します。

User.transaction do
  user.save!
  user.profile.create!
end

モデルのインスタンスに対して操作を行う場合は、インスタンスレベルでtransactionメソッドを呼び出すのが自然です。

@user.transaction do
  @user.update!(name: '新しい名前')
  @user.posts.create!(title: '最初の投稿')
end

もちろん、これらのルールは公式なものではありません。コードを読みやすくするための提案にすぎません。

次のステップ:Ruby on Railsプロジェクトのトランザクションを見直そう

Ruby on Railsでトランザクションを扱うためのこれらのTipsが役に立ったなら幸いです。

本記事では、データ整合性を高め、予期しない副作用なしにプロセスを実行するために、Railsのトランザクションを適切に設計することの重要性を解説しました。

しかし、適切なエラーハンドリングのポリシーは、トランザクションを使うときだけでなく、コードベース全体の品質向上にもつながります。コードがエラーを投げる可能性がある場面では、いつでもこのことを思い出してください。

今こそ、Ruby on Railsプロジェクトの設計におけるトランザクションを見直し、エラーを未然に防ぐ絶好のタイミングです。効率的で信頼性の高いデータベースとの通信を設計し、アプリケーションをより安定させましょう。

Happy coding!

P.S. Ruby Magicの記事を公開と同時にお読みになりたい方は、Ruby Magicニュースレターを購読すれば、記事を見逃すことはありません!

Ruby on Railsのトランザクションをマスターする:押さえておきたい5つの基本設計プラクティス

Paweł Dąbrowski(パヴェウ・ドンブロフスキ)

ゲスト執筆者のPawełはオープンソース愛好家であり、人間とコンピューターの両方に向けて10年以上の執筆経験を持つ成長志向のライターです。点と点をつなぎ、高品質なソフトウェアを作り、人やビジネスとの価値ある関係を築いています。

Paweł Dąbrowskiの記事一覧を見る

  1. Rackアプリケーションで受信メールを処理する「Incoming!」の使い方

    クライアントから突然電話が… クライアントは、自分のアプリで受信メールを処理したいと言っています。まるでBasecampのように。 「問題ないよ」と思うでしょう。多くのサービスがPOSTリクエスト経由でメールをアプリに送信してくれるのだから、簡単なはずです。本当に? 実は、そう甘くありません。 各サービスは独自のフォーマットを採用しています。パース済みのメールを送ってくるものもあれば、生データを送るものもあります。認証署名付きのサービスもあれば、そうでないものもあります。 この面倒な工程を丸ごとスキップして、標準的(よく知られた)なRubyオブジェクトとして直接扱えたら素敵だと思いま

  2. Rubyプログラムのデバッグと修正方法を徹底解説|スタックトレースの読み方からPry・Byebugまで

    書いたプログラムが、初回実行で思い通りに動くことってどれくらいありますか? 多くの場合、プログラムは期待どおりには動いてくれないもの。そんなときに頼りになるのがRubyのデバッグという技術です。原因を突き止めるための頼れる相棒といえるでしょう。 次のようなエラーメッセージを見たことはありませんか? undefined method some_method for nil:NilClass これは、nil値がコードの中に紛れ込んでしまったことを意味します。 本記事で紹介するテクニックを身につければ、この問題や似たようなトラブルにも自信を持って対処できるようになりますよ! エラーとスタックトレース