📝

[Rails][SQL] RailsでLEFT OUTER JOINする際のメモ

2025/01/04に公開

RailsでLEFT OUTER JOINをしてレコードを検索する必要がありました。
ただクエリ文はすぐにイメージがついたのですが、Railsでどう表現するかを少し戸惑ったのでメモを残します。
テーブル例はかなり適当に書いたので、テーブルのアソシエーションからJOINする箇所が参考になるかと思います。

環境

  • Rails 7.0.3

テーブル

Railsのschema例ですがTaskテーブル、SubTaskテーブルにアソシエーションがあります。
そしてSubTaskテーブルはtypeカラムがあり、SubTaskテーブルのレコードにタイプが複数あります。

  # Tasksテーブル
  create_table "tasks", force: :cascade do |t|
    t.string "title", null: false
    t.timestamps
  end

  # SubTasksテーブル
  create_table "sub_tasks", force: :cascade do |t|
    t.string "title", null: false
    t.string "task_id", null: false
    t.string "type", null: false, default: "TypeA"
    t.string "type_id", null: false
    t.timestamps
  end

  # Type_aテーブル
  create_table "type_as", force: :cascade do |t|
    t.string "title", null: false
    t.timestamps
  end

  # Type_bテーブル
  create_table "type_bs", force: :cascade do |t|
    t.string "title", null: false
    t.timestamps
  end

モデル

テーブル定義をモデルに反映しています。
そしてselected_type_a_categoryを定義しています。
selected_type_a_categoryはSubTaskモデルにアソシエーションとして追加しています。
内容としてはTypeAモデルとアソシエーションがあるSubTaskのみを取得する意図です。

class Task < ApplicationRecord
  belongs_to :sub_task
end

class SubTask < ApplicationRecord
  has_many :tasks, dependent: :destroy
  belongs_to :type_a, polymorphic: true
  belongs_to :selected_type_a_sub_task, -> {
    where(sub_tasks: { type: 'TypeA' })
  }, class_name: 'TypeA', foreign_key: 'type_id'
end

class TypeA < ApplicationRecord
  has_many :sub_tasks, as: :type_a
end

class TypeB < ApplicationRecord
  has_many :sub_task, as: :type_a
end

LEFT OUTER JOINでデータ取得

SubTaskからTypeAとタイトル名で一致するレコードを取得したい場合に、以下のように取得します。


Task
  .joins(:sub_tasks)
  .merge(
    SubTask
      .left_outer_joins(:selected_type_a_category)
      .where(type: 'TypeA') # .where(type: 'TypeA', type_a: { type_aテーブルで追加したい条件があれば追加 })
      .or(SubTask.where(title: 'hoge_title'))
  )

具体的には以下のようなSQLが発行されます。

SELECT tasks.*
FROM tasks
INNER JOIN sub_tasks ON sub_tasks.id = tasks.sub_task_id
LEFT OUTER JOIN type_as AS selected_type_a_categories 
  ON selected_type_a_categories.type_id = sub_tasks.type_id
  AND selected_type_a_categories.type = 'TypeA'
WHERE (sub_tasks.type = 'TypeA' OR sub_tasks.title = 'hoge_title');

INNER JOINでデータ取得

SubTaskからTypeAかつタイトル名が一致するレコードを取得したい場合に、以下のように取得します。

Task
  .joins(sub_tasks: :selected_type_a_category)
  .merge(
    SubTask.where(type: 'TypeA', title: 'hoge_title')
  )

具体的には以下のようなSQLが発行されます。

SELECT tasks.*
FROM tasks
INNER JOIN sub_tasks ON sub_tasks.id = tasks.sub_task_id
INNER JOIN type_as AS selected_type_a_categories
  ON selected_type_a_categories.type_id = sub_tasks.type_id
  AND selected_type_a_categories.type = 'TypeA'
WHERE sub_tasks.title = 'hoge_title';

まとめ

RailsでLEFT OUTER JOINを表現するとこのようになります。
今回試している中で思ったのが、
RailsはJOINする条件に絞り込み条件がもう少し書けたら効率的なクエリ(WHERE句の条件が減らせる)になるなと思いました。

Discussion