Active Record joinsについて

Active Recordのjoinsmergeについて調査したレポートです。

準備

まずは、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]]

以上で調査結果でした。ご静聴ありがとうございました。