ホーム DoRuby Ruby/Rails に潜む罠 (find_by編)
Ruby/Rails に潜む罠 (find_by編)
 

Ruby/Rails に潜む罠 (find_by編)

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

ActiveRecord::Baseのクラスメソッドの中で最も基本的なメソッドの内の1つである find_by 。Railsを使う案件では頻繁にソースコードの中に現れるだろう。 この至極基本的なメソッドに、実は罠が潜んでいて、稀な条件下では発動する可能性がある。 (条件がレアな為、ほとんど発動しないけれど)

find_by の基本的な使い方

モデル(モデルインスタンスではない)をレシーバにして、引数に条件をハッシュまたはSQL文字列をセット。
返り値はモデルインスタンスとなる。

例1

Artist.find_by(name: 'The Beatles')
# SELECT `artists`.* FROM `artists` WHERE `artists`.`name` = 'The Beatles' LIMIT 1

例2

Artist.find_by("name = 'The Beatles'")
# SELECT `artists`.* FROM `artists` WHERE (name = 'The Beatles') LIMIT 1

罠が発動する条件

モデルに対するテーブルが動的に変化する場合

class Artist < ActiveRecord::Base
  self.table_name = :artists_1 # デフォルトの参照テーブル
end
# 参照テーブルを切り替えるクラス
class TableSwitcher
  def self.exec
    if Artist.table_name == :artists_1
      Artist.table_name = :artists_2
    else
      Artist.table_name = :artists_1
    end
  end
end

この条件は、Railsの規約には沿っていない。つまりRailsの規約に全て沿っていれば、本記事の内容は気にする必要はない。
大半の案件はRailsの規約には沿うだろうが、大量データ更新の為、2つのテーブルを、TRUNCATE+INSERTしながら交互に参照を切り替えていく方針を取っている場合、この問題に直面する事になる。

罠が発動する条件下でfind_byを使うとどうなるか

Artist.table_name # => artists_1
Artist.find_by(name: 'The Beatles')
# SELECT `artists_1`.* FROM `artists_1` WHERE `artists_1`.`name` = 'The Beatles' LIMIT 1

TableSwitcher.exec # 参照テーブル切り替え

Artist.table_name # => artists_2
Artist.find_by(name: 'The Beatles')
# SELECT `artists_1`.* FROM `artists_1` WHERE `artists_1`.`name` = 'The Beatles' LIMIT 1

参照テーブルは切り替わっているはずなのに発行されるSQL中の参照しているテーブル名が切り替わっていない!
これでは、(テーブルを切り替えて)データを新しても、実際は参照するデータが古いままになってしまう可能性がある。

原因

デフォルトのfind_byメソッドは発行したSQLをキャッシュする。(ActiveRecord::StatementCache)
よって、参照テーブルを切り替えた後でも、切替前のキャッシュが残って、切替前のテーブルが参照されてしまう。

対処方法

罠が発動する条件に該当するモデルでは、デフォルトのfind_byが使用されないよう、クラス内でオーバーライドし、where+firstに書き換える。
whereメソッドはキャッシュされないので、この対処が可能となる。

class Artist < ActiveRecord::Base
  self.table_name = :artists_1 # デフォルトの参照テーブル

  class << self
    def find_by(*args)
      where(*args).first
    rescue
      super # 例外処理はスーパークラスに任せる
    end
  end
end

注意点

rubocop を導入している場合は、「where+first は find_by と書き直せ」という警告が出る。
意図的にwherefirstに分けている訳なので、該当部分だけ以下のようにrubocopチェックの対象外とする必要がある。

def find_by(*args)
  # rubocop:disable Rails/FindBy
  where(*args).first
rescue
  super
end
記事を共有

最近人気な記事