概要
ActiveRecord includesを使う際、selectに別名カラムの記述があると
その別名カラムの参照ができない事象があったので、記事にしました。
開発環境
- ruby 2.6.5
- Rails 5.2.4.4
ActiveRecordクラス 定義
class User < ApplicationRecord
has_one :house
end
class House < ApplicationRecord
belongs_to :user
belongs_to :house_info
end
class HouseInfo < ApplicationRecord
has_one :house
end
問題ない場合のincludes構文
- 問題なくuserインスタンスが取得できた (preloadの挙動になる)
User.includes(house: :house_info).select('users.id, users.created_at as user_registed_at')
SELECT users.id, users.created_at as user_registed_at FROM "users"
SELECT "houses".* FROM "houses" WHERE "houses"."user_id" = $1
[#<User:0x000055a87e374e58 id: 1>,
#<User:0x000055a87e3742c8 id: 2>,
#<User:0x000055a87e37f858 id: 3>,
#<User:0x000055a87e37ecc8 id: 4>,
#<User:0x000055a87e37e1b0 id: 5>]
- selectで指定した別名カラムの値も取得できる
User.includes(house: :house_info).select('users.id, users.created_at as user_registed_at').first.user_registed_at
SELECT users.id, users.created_at as user_registed_at FROM "users"
SELECT "houses".* FROM "houses" WHERE "houses"."user_id" = $1
=> "2021-02-05T13:02:17.754+09:00"
問題ありのincludes構文
includesしたテーブルに対し、検索条件(where)を追記してみた
- house_infoのownerカラムをwhere条件にいれると left outter joinとなり、テーブルそれぞれのカラムが別名のsqlが発行される(eager_loadの挙動になる)
User.includes(house: :house_info).where(house_infos: { owner: '太郎' }).select('users.id, users.created_at as user_registed_at')
SELECT users.id, users.created_at as user_registed_at, "users"."id" AS t0_r0, "users"."name" AS t0_r1, "interviews"."creator_id" AS t0_r2, "houses"."user_id" AS t0_r3, "houses"."house_info_id" AS t0_r4, "house_infos"."address" AS t0_r5, "house_infos"."owner" AS t0_r6
FROM "users" LEFT OUTER JOIN "houses" ON "houses"."interview_id" = "users"."id" LEFT OUTER JOIN "house_infos" ON "house_infos"."id" = "houses"."house_info_id" WHERE "house_infos"."owner" = '太郎'
[#<User:0x000055a879e296c8
id: 1,
created_at: 2020/12/12>
]
- select で別名指定した
user_registed_at
はsql上、存在するが、モデルから参照できない。
User.includes(house: :house_info).where(house_infos: { owner: '太郎' }).select('users.id, users.created_at as user_registed_at').first.user_registed_at
NoMethodError: undefined method `user_registed_at' for #<User:0x000055a87c1a93c8>
from /bundle/gems/activemodel-5.2.4.4/lib/active_model/attribute_methods.rb:430:in `method_missing'
includesをやめて解決
-
house
とhouse_info
を joinし、preloadでキャッシュするようにした
User.joins(house: :house_info).preload(house: :house_info).where(house_infos: { owner: '太郎' }).select('users.id, users.created_at as user_registed_at')
SELECT users.id, users.created_at as user_registed_at FROM "users" INNER JOIN "houses" ON "houses"."user_id" = "users"."id" INNER JOIN "house_infos" ON "house_infos"."id" = "houses"."house_info_id" WHERE "house_infos"."owner" = '太郎'
SELECT "houses".* FROM "houses" WHERE "houses"."user_id" IN ($1, $2, $3, $4, $5)
SELECT "house_infos".* FROM "house_infos" WHERE "house_infos"."id" IN ($1, $2, $3, $4, $5)
[#<User:0x000055a87d6fb410 id: 1>,
#<User:0x000055a87d6fa8f8 id: 2>,
#<User:0x000055a87d6f9de0 id: 3>,
.
.
- 各モデルから
user_registed_at
が参照できるようになった。
User.joins(house: :house_info).preload(house: :house_info).where(house_infos: { owner: '太郎' }).select('users.id, users.created_at as user_registed_at').first.user_registed_at
=> "2021-02-05T13:05:47.225+09:00"
includesの挙動について調べてみた
-
includesはjoinsがなければpreloadと同じ挙動となる
-
明示的にeager_loadと同じ挙動にするためにはreferencesというメソッドを併用する必要
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-references
User.includes(house: :house_info).references(house: :house_info).select('users.id, users.created_at as user_registed_at')
SELECT users.id, users.created_at as user_registed_at, "users"."id" AS t0_r0, "users"."name" AS t0_r1, "interviews"."creator_id" AS t0_r2, "houses"."user_id" AS t0_r3, "houses"."house_info_id" AS t0_r4, "house_infos"."address" AS t0_r5, "house_infos"."owner" AS t0_r6
FROM "users" LEFT OUTER JOIN "houses" ON "houses"."interview_id" = "users"."id" LEFT OUTER JOIN "house_infos" ON "house_infos"."id" = "houses"."house_info_id"
なぜ?別名カラムをモデルから参照できなかったのか?
https://github.com/rails/rails/issues/34889
バグだったらしく、Rails6.0.3で治っているよう。
最後に
includesはケースによって
preloadまたはeager_loadの挙動をするのは知っていたが、
selectで指定した別名カラムの参照問題は知らず、、1日ハマっていた。
やっぱり、ActiveRecordはハマりポイントが多そう。