🐰

Railsの複数データベースでconnected_toを使う際はクエリの実行タイミングに注意しよう

2023/07/17に公開

Railsの複数データベース機能利用時に意図せずに無駄なクエリ(SQL)を実行しているケースに遭遇したため、そのことについて書いていきます。

TL;DR

connected_toのブロックでActiveRecord::Relationを返す場合、その時点でクエリが実行される。
そのため以下のようにconnected_toのブロックの戻り値に対してリレーションを追加して絞り込みを行うような書き方は無駄にSQLを実行してしまうため基本的には避けた方がいい。

relation = ActiveRecord::Base.connected_to(role: :reading) do
             HogeModel.where(condition1)
           end # この時点で HogeModel.where(condition1) の条件でSQLが実行される
relation.where(condition2)

前提知識

Active Recordの遅延実行

Active Recordはそのデータが必要になるタイミングまでクエリを実行しないようにする遅延実行の仕組みがあります。
以下のようなコードの場合、SQLが実行されるのはeachのタイミングです。

users = User.where(name: 'hoge')
users.each do |user|
  # 何か処理
end

また、以下のように条件をつなげたとしてもSQLが実行されるのはeachのタイミングの1度です。

users = User.where(name: 'hoge')
users = users.where(status: 'active')
users.each do |user|
  # 何か処理
end

connected_toによるコネクションの手動切り替え

詳しくはRailsガイドのコネクションを手動で切り替えるを見てもらうのがいいですが、簡単にふれます。

Railsアプリケーションから複数のDBに接続する場合に、connected_toメソッドで処理中にどのDBに接続するかを個別に選択することができます。
例えば書き込み用のwriter DBと読み見込み用のreplica DBがあり、特定の処理でreplica DBに対してクエリを実行したい場合は以下のようにすることで実現できます。

ActiveRecord::Base.connected_to(role: :reading) do
  # このブロック内のコードはすべてreadingロール(replica DB)に対してSQLが実行される
  User.where(name: 'hoge')
end

connected_to使用時に発生し得る問題

以下のようなコードでユーザーを絞り込んでSQLを実行する場合、 to_a でのタイミングで実行されます。

users = User.where(status: 'active')
# usersには ActiveRecord::Relation が返り、そこにさらに条件を追加する
users = users.where(name: 'hoge').limit(10)

users.to_a # このタイミングでSQLが実行される

このクエリをreplica DBに対し実行するために

users = ActiveRecord::Base.connected_to(role: :reading) do
          User.where(status: 'active')
        end # この時点でSQLが実行される
users = ActiveRecord::Base.connected_to(role: :reading) do
          users.where(name: 'hoge').limit(10)
        end # この時点でSQLが実行される

users.to_a

のように書き換えたとします。

to_aのタイミングでのみSQLを実行してほしいですが、一つ目のconnected_toブロックを抜ける際にUser.where(status: 'active')の条件でSQLが実行されてしまいます。

本来はwhere(name: 'hoge').limit(10)をつけて取得するレコードを絞って実行したいのにactiveなユーザーを全て取ってくるようなSQLが実行されます。usersテーブルが巨大だった場合にはかなり重たいSQLが実行されることが想定されます。

このような挙動となるのはconnected_toは、ブロックでActiveRecord::Relationを返す場合、その時点で強制的にクエリを実行するためです。
ref: https://github.com/rails/rails/pull/38339

最終的な条件でのみSQLを実行したい場合は以下のようにブロック内でクエリの組み立てを完成さる必要があります。

users = ActiveRecord::Base.connected_to(role: :reading) do
          relation = User.where(status: 'active')
          relation.where(name: 'hoge').limit(10)
        end # この時点でSQLが実行される

users.to_a

この例のようにシンプルな処理であれば、わざわざconnected_toを複数に分けて書くようなことはしないかもしれません。しかし、複雑なリレーションを組み立てる時などは注意が必要です。

実際に私自身が関わっているシステムで発生しました。connected_toを使う際は気をつけましょう。

GitHubで編集を提案

Discussion