Active Record joinsについて
Active Recordのjoinsとmergeについて調査したレポートです。
準備
まずは、migrationファイルを設定
class CreateUserPlans < ActiveRecord::Migration[5.2] def change create_table :user_plans do |t| t.integer :user_id t.integer :plan_code end end end
class CreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :name t.string :state end end end
続いて、Userモデルを書きます
class User < ApplicationRecord has_many :user_plans scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } has_many :active_plans, -> { where plan_code: 2 }, class_name: UserPlan.name end
そして、Userと1対多の関連を持つUserPlanも
class UserPlan < ApplicationRecord belongs_to :user scope :active_plan, -> { includes(:user).where(users: { state: 'active' }) } scope :po, -> { where(plan_code: 1) } end
最後に、seed.rbを下記のようし、db:migrate && db:seedで準備完了です。
User.create!( name: "Admin", state: "active" ) User.create!( name: "Imaharu", state: "inactive" ) 8.times do |n| active = [true, false].sample ? "active" : "inactive" User.create!( name: "User#{n+3}", state: active ) end 40.times do |n| UserPlan.create!( user_id: rand(8) + 1, plan_code: rand(3) + 1 ) end
本題
これでは、実験してみましょう
User.joins(:user_plans).each do |user| p "user_id: #{user.id}, state: #{user.state}" end SELECT "users".* FROM "users" INNER JOIN "user_plans" ON "user_plans"."user_id" = "users"."id"
joinsは、INNER JOINを実行していますね。
次に、active scopeを追加してクエリを発行して見るとどうなるのでしょうか?
User.active.joins(:user_plans)
を実行するとINNER JOINのクエリ結果に対してWHERE句で絞り込みをしています。Limitは無視して下さい。consoleの設定で治りそうですか
ちなみに、User.joins(:user_plans).active
としても同じクエリが発行されます。これは、Active Recordがおそらく優先順位も持っていてうまく解釈してるのではないかと思います。そのうち、 Gemの中身を覗いてみたいです。
SELECT "users".* FROM "users" INNER JOIN "user_plans" ON "user_plans"."user_id" = "users"."id" WHERE "users"."state" = ? LIMIT ?
続いて、mergeを使ってみましょう。mergeは結合したモデル。ここでは、UserPlanに対してアクションを可能にします。
ここで、ポイントなのだが、activeと並列のWHERE句にある点です。
このことから、mergeはINNER JOINで得られた結果に評価を加えていることがわかります。
User.active.joins(:user_plans).merge(UserPlan.po) SELECT "users".* FROM "users" INNER JOIN "user_plans" ON "user_plans"."user_id" = "users"."id" WHERE "users"."state" = ? AND "user_plans"."plan_code" = ? LIMIT ? [["state", "active"], ["plan_code", 1], ["LIMIT", 11]]
最後に、INNER JOIN前に絞り込む方法をみていきましょう
has_many :active_plans, -> { where plan_code: 2 }, class_name: UserPlan.nameが該当箇所です。
ANDで条件をしてできていることが確認できました。上記の関連付けに特別な名前がありそうな雰囲気ですが、ちょっとわからないです。 もし、知っていればコメントして下さい!
User.joins(:active_plans) SELECT "users".* FROM "users" INNER JOIN "user_plans" ON "user_plans"."user_id" = "users"."id" AND "user_plans"."plan_code" = ? LIMIT ? [["plan_code", 2], ["LIMIT", 11]]
以上で調査結果でした。ご静聴ありがとうございました。