Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails3のfind系メソッドと注意のまとめ

Rails3では,ActiveRecordを使ったデータの取得方法にfind, find_by_XXX, find_all_by_XXX, all, whereでの絞り込みなど色々ありますが,それぞれ微妙に異なった挙動をします.
ちょろいサンプルアプリを書く位ならあまり気にならない所なのですが,きちんとアプリを書く上では以外と大事なところ(後述)なので,ここいらでまとめておきます.

Rails使っている人で

hoge = Hoge.find(params[:id]) || Hoge.new

とか書いて思った様に動かないぞ?とかハマった人向けです.

取り上げるのはfind, find_by_XXX, all, find_all_by_XXX, whereです.Railsのバージョンは3.2.8を使っています.

以下の説明では,以下のクラスが存在することを前提とします.

class Hoge < ActiveRecord::Base
end

ActiveRecord#find

Railsデフォルトのscaffoldが使うfindです.

hogesテーブルからID: 111のレコードを取得する.

hoge = Hoge.find(111)
  • 当該IDのレコードが存在する場合,当該クラスのインスタンス(この場合はHoge)を返す
  • 当該IDのレコードが存在しない場合,ActiveRecord::RecordNotFoundをraiseする.この例外をcontroller側でrescueしない場合,404エラーになる

ActiveRecord#find_by_XXX

1レコード取り出したい場合に便利なメソッドです.XXXはフィールド名になり,後ろに_and_YYYとどんどん条件を追加していくことが可能です.ORDER BY id DESCを付けて検索が行われるため,複数レコードがある場合は最後にINSERTされたレコードが返ります

hogesテーブルからID: 111のレコードを取得する.

hoge = Hoge.find_by_id(111)
  • 当該IDのレコードが存在する場合,当該クラスのインスタンス(この場合はHoge)を返す
  • 当該IDのレコードが存在しない場合,nilを返す

nilを返すため,その後にメンバ変数にアクセスする場合には,nilチェックしないとmethod not foundが出ます.

ActiveRecord#all

当該テーブルの全レコードを取り出します.scaffoldの#indexで使われています.

hogesテーブルから全てのレコードを取得する.

hoges = Hoge.all
  • 当該クラスのインスタンスが入ったArrayを返す
  • レコードが存在しない場合,空のArray []を返す

allはその時点でSQLを発行してArrayを返すため,後ろにwhereやorder条件を付けることができません.割と不便.

ActiveRecord#find_all_by_XXX

allの条件指定版です.

hogesテーブルからID: 110, 111のレコードを取得する.

hoges = Hoge.find_all_by_id([110, 111])
  • 当該クラスのインスタンスが入ったArrayを返す
  • レコードが存在しない場合,空のArray []を返す

allと似てますね.

ActiveRecord#whereを始めとするArel系メソッド

whereを使ってfind系と同じようなことをすることは可能なので,参考までに違いを挙げておきます.
hogesテーブルからID: 111のレコードを取得する.

hoge = Hoge.where(:id => 111).first

# prepared statement風に
hoge = Hoge.where("id = ?", 111).first
# joinがある場合はテーブル名を指定して以下の様にする.scope切るときはテーブル名指定した方が無難
hoge = Hoge.where("hoges.id = ?", 111).first

hogesテーブルからID: 110, 111のレコードを取得する.

hoges = Hoge.where(:id =>[110, 111])
hoges.each do |h|
  # hに対する操作
end

# prepared statement風に
hoges = Hoge.where("id IN (?)", [110, 111])
# joinがある場合はテーブル名を指定して以下の様にする.scope切るときはテーブル名指定した方が無難
hoges = Hoge.where("hoges.id IN (?)", [110, 111])
  • whereそのものはActiveRecord::Relationを返す
  • ActiveRecord::RelationはEnumerableに似たアクセスができる(#eachとか#firstとか#lastとか)
  • ActiveRecord::Relationは遅延評価されるので,レコードが必要になるまでクエリは投げられない(Viewのレンダリング中にクエリが発行されることがある)
  • ActiveRecord::Relationにはさらにwhereorderなどを繋いで行くことができる.2回以上whereを呼べばAND条件で結びつけられる

ActiveRecord::Relationを使った方が集合を取ってくるときには便利です.ただ,注意として,prepared statement風にINを使って書く場合,引数が空配列になると「WHERE id IN (NULL)」で実行されてしまうので,NULL許可しているフィールドにWHERE INを使う場合には注意が必要です.

以下,参考までにそれぞれのwhere引数と,どんなSQLが実行されるかをまとめておきます.

# SELECT `hoges`.* FROM `hoges` WHERE `hoges`.`id` = 111 ORDER BY id DESC
Hoge.where(:id => 111)

# SELECT `hoges`.* FROM `hoges` WHERE `hoges`.`id` IN (111) ORDER BY id DESC
Hoge.where(:id => [111])

# SELECT `hoges`.* FROM `hoges` WHERE `hoges`.`id` IN (111, 112) ORDER BY id DESC
Hoge.where(:id => [111, 112])

# SELECT `hoges`.* FROM `hoges` WHERE (id = 111) ORDER BY id DESC
Hoge.where("id = ?", 111)

# SELECT `hoges`.* FROM `hoges` WHERE (id IN (111)) ORDER BY id DESC
Hoge.where("id IN (?)", [111])

# SELECT `hoges`.* FROM `hoges` WHERE (id IN (NULL)) ORDER BY id DESC
Hoge.where("id IN (?)", [])

# SELECT `hoges`.* FROM `hoges` WHERE (hoges.id IN (111)) ORDER BY id DESC
Hoge.where("hoges.id IN (?)", [111])

どんなときにハマるか?

例としては,漫然と

hoge = Hoge.find(params[:id]) || Hoge.new

というコードを書いたときにハマります.このコードの意図は「params[:id]のレコードが無ければHogeのnewしたインスタンスをhogeに代入する」ですが,実際に動かしてみるとparams[:id]のレコードが無い場合,404エラーになります(前述の通りActiveRecord::RecordNotFoundがraiseされるため).
この場合,

hoge = Hoge.find_by_id(params[:id]) || Hoge.new

と書かなければいけないんですね.この辺,割とハマることが多いので注意ですね.

※10/19 find使ったときのハマる具体例を追加しました.


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。