LoginSignup
1
0

More than 5 years have passed since last update.

Rails5.1で順序依存なくSQLを組み立てる_scope編

Last updated at Posted at 2017-11-28

 Rails5.2で解決済み【追記】

Avoid duplicate clauses when using #or #29950
で修正されており、取り込まれた5.2で解決しました

 概要

自身参照scopeの場合、Relationの繋ぎ方(順序)によってはSQLの重複が発生した
意図しない挙動になるのでなるべく避けたい

具体的な挙動

実際に自身参照scopeが必要な場合は複雑なものになるが検証のため以下を用いる

class User
  scope :fuga, -> { where(id: where(type: 'fuga')) }
end

scope前に条件がある場合はscope内の自己参照部分にもその条件が入る(id=1の重複)

User.where(id: 1).fuga.to_sql
=> "SELECT `users`.* FROM `users` 
    WHERE `users`.`id` = 1 
    AND `users`.`id` IN (
        SELECT `users`.`id` FROM `users` 
        WHERE `users`.`id` = 1 AND `users`.`type` = 'fuga'
    )"

scope後に条件がある場合はscope内の自己参照部分にはその条件が入らない

User.fuga.where(id: 1).to_sql
=> "SELECT `users`.* FROM `users` 
    WHERE `users`.`id` IN (
        SELECT `users`.`id` FROM `users` 
        WHERE `users`.`type` = 'fuga'
    ) AND `users`.`id` = 1"

条件が重なるだけで出力結果に違いはないが順序依存で重複の有無が変動する

問題か否か

例えば重いサブクエリの場合は重複が問題になるかもしれない

User.large_subquery.fuga.to_sql #=> 遅くなるかも
User.fuga.large_subquery.to_sql #=> 通常

例えば大量データのテーブルならindex次第で重複が有効かもしれない

User.cutdown_subquery.fuga.to_sql #=> 速くなるかも
User.fuga.cutdown_subquery.to_sql #=> 通常

ただ、上記のような順序依存があるのが好ましくないと感じる
作った当時は調整できても後で修正する時に事故になる危険性があり保守性が落ちる
調整が必要ならその調整が崩れないように宣言しておくほうが安心する

# データが多く高速化のためサブクエリにも適用しておく必要がある
scope :cutdowned_fuga, -> { where(id: cutdown_subquery.fuga) }

順序依存を無くす方法

引き継がないことを定義すれば良い

class User
  scope :fuga, -> { where(id: unscoped.where(type: 'fuga')) }
end

そうすれば順序を問わず重複しない

User.where(id: 1).fuga.to_sql
=> "SELECT `users`.* FROM `users` 
    WHERE `users`.`id` = 1 AND `users`.`id` IN (
      SELECT `users`.`id` FROM `users` WHERE `users`.`type` = 'fuga'
    )"

User.fuga.where(id: 1).to_sql
=> "SELECT `users`.* FROM `users` 
    WHERE `users`.`id` IN (
        SELECT `users`.`id` FROM `users` WHERE `users`.`type` = 'fuga'
    ) AND `users`.`id` = 1"

注意: 間違えやすい方法

クラスから呼び新しいRelationにすれば良いと考えやすい

class User
  scope :fuga, -> { where(id: User.where(type: 'fuga')) }
end

実際には前条件が付いてしまう、同じRelationキャッシュを使っている?

User.where(id: 1).fuga.to_sql
=> "SELECT `users`.* FROM `users` 
    WHERE `users`.`id` = 1 AND `users`.`id` IN (
        SELECT `users`.`id` FROM `users` 
        WHERE `users`.`id` = 1 AND `users`.`type` = 'fuga'
    )"

参考

残念ながら見つかりませんでした
unscoped で調べても default_scope の解除方法しか出てこない...

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0