2013年11月16日土曜日

Railsで多階層のテーブル結合をfindするサンプル


上図のようにテーブルが多階層(3階層以上)で結合されている場合に、ActiveRecord の findメソッドでconditionsを指定する方法を考えてみる。
rails generate model gun name:string
rails generate model shidan gun:references name:string
rails generate model rentai shidan:references name:string
rake db:migrate
サンプルデータも投入しておく。
rails c
Gun.create(:name=>'1-gun')
Gun.create(:name=>'2-gun')
Shidan.create(:gun_id=>1, :name=>'1-shidan')
Shidan.create(:gun_id=>2, :name=>'2-shidan')
Rentai.create(:shidan_id=>1, :name=>'1-rentai')
Rentai.create(:shidan_id=>2, :name=>'2-rentai')
この状態で、2階層であれば、findメソッドで下記のようなconditionsを指定できる。
Rentai.all(:include => :shidan, :conditions => ["shidans.id = ?", 2])
しかし、なんとなく直感的に3階層の場合のconditionsを下記のように設定しても、うまくいかない。
Rentai.all(:include => :shidan.gun, :conditions => ["shidans.gun.id = ?", 2])

NoMethodError: undefined method `gun' for :shidan:Symbol
includeオプションについては、下記のページに記述があったので、 

Ruby on Railsあれこれ/findメソッドの:includeオプションの書き方
http://winter-tail.sakura.ne.jp/pukiwiki/index.php?Ruby%20on%20Rails%A4%A2%A4%EC%A4%B3%A4%EC%2Ffind%A5%E1%A5%BD%A5%C3%A5%C9%A4%CE%A1%A7include%A5%AA%A5%D7%A5%B7%A5%E7%A5%F3%A4%CE%BD%F1%A4%AD%CA%FD

下記のようにincludeを書き換えてみたものの、やはりダメである。
Rentai.all(:include => {:shidan => :gun}, :conditions => ["shidans.gun.id = ?", 2])

ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: shidans.gun.id: SELECT "rentais"."id" AS t0_r0, "rentais"."shidan_id" AS t0_r1, "rentais"."name" AS t0_r2, "rentais"."created_at" AS t0_r3, "rentais"."updated_at" AS t0_r4, "shidans"."id" AS t1_r0, "shidans"."gun_id" AS t1_r1, "shidans"."name" AS t1_r2, "shidans"."created_at" AS t1_r3, "shidans"."updated_at" AS t1_r4, "guns"."id" AS t2_r0, "guns"."name" AS t2_r1, "guns"."created_at" AS t2_r2, "guns"."updated_at" AS t2_r3 FROM "rentais" LEFT OUTER JOIN "shidans" ON "shidans"."id" = "rentais"."shidan_id" LEFT OUTER JOIN "guns" ON "guns"."id" = "shidans"."gun_id" WHERE (shidans.gun.id = 2)
ダメではあったものの、出力されたSQLには有益な情報が含まれているので、余計な個所を削って人間にとって見やすいように成形してみる。
SELECT 
  rentais.id,
  rentais.shidan_id,
  rentais.name,
  rentais.created_at,
  rentais.updated_at, 
  shidans.id,
  shidans.gun_id,
  shidans.name,
  shidans.created_at,
  shidans.updated_at,
  guns.id,
  guns.name,
  guns.created_at,
  guns.updated_at
FROM rentais
LEFT OUTER JOIN shidans ON shidans.id = rentais.shidan_id
LEFT OUTER JOIN guns ON guns.id = shidans.gun_id
WHERE (shidans.gun.id = 2)
結局のところ、conditonsではSQLのWHERE句の内容を切り出しているに過ぎず、rubyだからといってオブジェクト間の階層を難しく意識せず、SQLに忠実に、"guns.id"で指定すれば良いようである。

下記のように書き直すことで、意図した検索結果を得ることができた。
Rentai.all(:include => {:shidan => :gun}, :conditions => ["guns.id = ?", 2])

=> [#<Rentai id: 2, shidan_id: 2, name: "2-rentai", created_at: "2013-11-16 07:12:21", updated_at: "2013-11-16 07:12:21">]