【Ruby on Rails Advent Calendar 2019】ActiveRecord で後から where クエリを上書きする【2日目】
Ruby on Rails Advent Calendar 2019 2日目の記事になります。
今回は最近ハマっているクエリメソッド付けたり外したりしていることを簡単に書いてみようかと思います。
サンプルモデル
今回は以下のような scope
を定義したモデルを例として説明していきたいと思います。
class User < ActiveRecord::Base # 管理者を絞り込む scope scope :administer, -> { # 以下の条件を管理者として扱う where(active: true).where.not(email: nil) } end # 管理者一覧を取得する puts User.administer.to_sql # => SELECT "users".* # FROM "users" # WHERE "users"."active" = TRUE # AND "users"."email" IS NOT NULL
これ自体はとてもシンプルなつくりになっていますね。
スコープを呼び出した後に rewhere
を使ってクエリを上書きする
.administer
を呼び出した後にその条件を書き換えたい場合があると思います。
しかし、普通に where
をチェーンしただけであればクエリは上書きされずに AND
で結合されます。
# where をチェーンした場合は "users"."active" = FALSE が結合される puts User.administer.where(active: false).to_sql # => SELECT "users".* # FROM "users" # WHERE "users"."active" = TRUE # AND "users"."email" IS NOT NULL # AND "users"."active" = FALSE -- このクエリが追加されてしまう
こういうケースでは where
ではなくて rewhere
を使ってクエリを上書きする事が出来ます。
# rewhere を使うことですでに追加済みのクエリを上書き出る puts User.administer.to_sql # => SELECT "users".* # FROM "users" # WHERE "users"."email" IS NOT NULL # AND "users"."active" = FALSE
これで既存のクエリを上書きすることが出来ます。
rewhere
の他にも reorder
とういうクエリメソッドがあり、こちらは ORDER
を上書き出来ます。
scope
を使うと予め複雑なクエリを定義しておく事が出来るのですが、一部だけクエリを書き換えたい場合などに rewhere
や reorder
が使えることを覚えておくとよいと思います。
注意点
この rewhere
ですが joins
等で複数のテーブルをクエリで使用している場合に意図しない動作が発生する事があります。
どういうことかと言うと例えば以下のような .joins
して .merge
している場合に "users"."active" = TRUE
と "blogs"."active" = TRUE
の 2つのクエリが追加されます。
class User < ActiveRecord::Base scope :administer, -> { where(active: true).where.not(email: nil) } has_one :blog end class Blog < ActiveRecord::Base scope :active, -> { where(active: true) } end # User.administer と Blog.active を結合する # 両方のテーブルのクエリがそれぞれ追加される puts User.administer.joins(:blog).merge(Blog.active).to_sql # => SELECT "users".* # FROM "users" # INNER JOIN "blogs" # ON "blogs"."user_id" = "users"."id" # WHERE "users"."active" = TRUE -- ここと # AND "users"."email" IS NOT NULL # AND "blogs"."active" = TRUE -- ここ
このようなクエリに対して rewhere(active: false)
を呼び出すと意図しない挙動になります。
# rewhere(active: false) すると "blogs"."active" が削除される puts User.administer.joins(:blog).merge(Blog.active).rewhere(active: false).to_sql # => SELECT "users".* # FROM "users" # INNER JOIN "blogs" # ON "blogs"."user_id" = "users"."id" # WHERE "users"."email" IS NOT NULL # AND "users"."active" = FALSE
なぜか "blogs"."active"
が削除され、 "users"."active"
だけが FASLE
に置き換わっています。
これは rewhere
がテーブルを考慮してないからになります。
rewhere
の実装を見てみると
def rewhere(conditions) unscope(where: conditions.keys).where(conditions) end
となっています。
この unscope
が肝で、このクエリメソッドを使用することで特定のカラムの where
を削除することが出来ます。
# unscope すると特定の where が削除される puts User.administer.joins(:blog).merge(Blog.active).unscope(where: :active).to_sql # => SELECT "users".* # FROM "users" # INNER JOIN "blogs" # ON "blogs"."user_id" = "users"."id" # WHERE "users"."email" IS NOT NULL
この時に unscope
はテーブルを考慮せずに "users"
と "blogs"
の両方のクエリが削除されしまいます。
結果的にその後に呼び出される where(active: false)
だけが追加されることになります。
このように複数のテーブルを参照しているクエリに対して rewhere
を呼び出す場合は注意が必要です。
ちなみにこの問題はもしかすれば今後改善されるかもしれません。
rewhereでunscopeされるのがテーブル単位じゃないことは把握はしているので6.1には直せると思います…
— Ryuta Kamizono (@kamipo) 2019年11月29日