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 より