find_by_sql で派生カラムを作ると update_attributes で更新できてしまう
ActiveRecord::Base
を継承したモデルクラスで,テーブルに存在しないカラムに対して #update_attributes
すると ActiveRecord::UnknownAttributeError
例外が発生します.たとえば,
な foos テーブルに相当する class Foo < ActiveRecord::Base
について,存在しないカラム bar
を更新しようとすると怒られます.
irb(main):007:0> foo
=> #<Foo id: 1, title: "hogege", created_at: "2011-11-17 00:10:38", updated_at: "2011-11-17 00:10:38">
irb(main):008:0> foo.update_attributes(bar: 'hogege')
ActiveRecord::UnknownAttributeError: unknown attribute: bar
(以下略)
あたり前ですが unknown attribute: bar
と言われています.
しかし,ActiveRecord のある機能を使っているときには, 存在しないカラムに対して #update_attributes
を実行してもエラーになりません.たとえば,先程の foo
に対して同じように #update_attributes
を実行しているのですが,
irb(main):007:0> foo
=> #<Foo id: 1, title: "hogege", created_at: "2011-11-17 00:10:38", updated_at: "2011-11-17 00:10:38">
irb(main):008:0> foo.update_attributes(bar: 'hogege')
=> true
と,今度はエラーになりません.この二つ目の例は,最初の例と何が違うのでしょうか.
find_by_sql を使って派生カラムをつくる
実は,最初の例は Foo.find
で作成したオブジェクト,二つ目の例は Foo.find_by_sql
で作成したオブジェクトです.Foo.find
で作成したオブジェクトのカラム属性は foos
テーブルのカラム由来のものだけですが,Foo.find_by_sql
を利用すると,SQL の JOIN を利用して foos
テーブル以外のカラムも属性として利用できます.この,モデルクラスのテーブル以外に由来するカラムを派生カラム(derived column)*1と呼びます.
種明かしをすると,二つ目の例の foo
は,
irb(main):001:0> foo = Foo.find_by_sql('select *, bars.title AS bar FROM foos, bars WHERE foos.id = bars.id').first
Foo Load (0.4ms) select *, bars.title AS bar FROM foos, bars WHERE foos.id = bars.id
=> #<Foo id: 1, title: "hogege", created_at: "2011-11-17 00:10:38", updated_at: "2011-11-17 00:10:38">
として find_by_sql
で派生カラム bar
を用意していたのです.
find_by_sql を使うときの注意点
注意しないといけないのは,派生カラムに対して #update_attributes
などで更新してもデータベースのカラムは更新されないという点です.さらにやっかいなのは,カラムの値は変わらないのに,update_at
は更新されるところです.ちょっとやってみましょう.
irb(main):002:0> foo.update_attributes(bar: 'hogegegegege')
(0.5ms) UPDATE "foos" SET "updated_at" = '2011-11-17 00:22:32.092490' WHERE "foos"."id" = 1
=> true
irb(main):003:0> Bar.find(1).title
Bar Load (0.3ms) SELECT "bars".* FROM "bars" WHERE "bars"."id" = ? LIMIT 1 [["id", 1]]
=> "hogege"
find_by_sql
で作成した foo
の派生カラムを #update_attributes
で更新しようとしたけど実際には更新されない例なのですが,2 行目に表示されている SQL 文を見れば分かるように,bars
テーブルは更新せずに foos
テーブルの update_at
だけを UPDATE
しています.
分かっていれば何のことはないのですが,忘れていると
#update_attribute
したのにカラムが更新されていない,更新したつもりがないのにupdate_at
が書き変わっている
とデバッグのときに小一時間悩むことになるので気をつけましょう.と,同時に,find_by_sql
を利用するモデルクラスのメソッドは,ユニットテストで想定外の動作をしないか要確認です.
終わりに
本エントリでは、ActiveRecord の find_by_sql と update_attributes と update_at カラムとの素敵な関係についてお話ししました。find_by_sql なんて Rails の本線というか支線的なメソッドで、Rails 開発者でも多用する人は少ないと思います。しかし、SQL を駆使してデータベースを活用するときに便利なメソッドですので覚えておいて損はありません。私が好きな ActiveRecord のメソッドの一つです。本線を走れば高速に走れるけど支線を経由する自由を認めているのも Rails の魅力の一つですね。
どうでもいい話ですが、本線から外れるという意味で、やっとこのブログタイトルらしいエントリが書けました。
バージョン
この記事を書いたときの Rails のバージョンは 3.1.1 です。
*1:Agile Web Development with Rails 4th ed. の p.289 より