リモート開発メインのソフトウェア開発企業のエンジニアブログです

Rails の has_many through で scope を使う

Active Record の関連付け(アソシエーション = associtaion)は色んな機能があります。 今回はタイトル通り、has_many :throughscope を併用する方法を紹介します。

has_one, has_many, belongs_to 辺りの違いは理解していることを前提とします。

has_many :through の説明

最初に、 has_many :through について説明します。Rails ガイドを見た方が早いと思うので、まずはリンクを貼っておきます。

Active Record の関連付け – Railsガイド

患者(patient)、医師(physician)、診察予約(appointment)の3つのモデルがそれぞれ

  • patient:appointment = 1:n
  • physician:appointment = 1:n
  • patient:physician = m:n

という関係になっています。最後の patient:physician の関係は、appointment を通して(through)の関係のため、

  • A physician has many patients through appointments
  • A patient has many physicians through appointments

という意味で has_many :through association と呼ばれます。

Moba Pro

本題

やりたいこと

さて、先ほどの Rails ガイドの例を少し発展させてみます。

例えば、ある医師 (physician) の患者 (patient) のうち、現在時刻〜今日の終わりまでの appointment をもっている患者だけを取り出したいと思います。

where 句を使うと以下の通りです。

phy = Physician.find 1
now = Time.current
phy.patients.where(appointments: {appointment_date: now..now.end_of_day})

中間テーブルに scope を設定

現在時刻〜今日の終わりまでの appointment というのはよく使う条件なので、scope を設定する事にします。 Appointment モデルには、以下のような scope を追加します。scope 名は :today とでも付けておきましょう。

scope :today, -> { now = Time.current; where(appointment_date: now..now.end_of_day) }

ここまでは問題無いと思います。

scope を使って書き換え

それでは先ほどのクエリーを、新たに設定した scope を使って書き直してみましょう。

まずはダメな例から。

phy = Physician.find 1
phy.patients.today # ダメ

何がダメかというと、today という scope は、patients ではなく中間テーブルである appointments に設定されているからです。

これを解決する方法はいくつかあるようですが、私は以下のようにしました。

class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
  # 新規に以下の2行を追加
  has_many :todays_appointments, -> { today }, class_name: 'Appointment'
  has_many :todays_patients, through: :todays_appointments
end

ポイントは、中間テーブルに対する has_many で scope を使う事です。

その上で以下のようにします。

phy = Physician.find 1
phy.todays_patients # OK

まとめ

中間テーブルを使った m:n の関係を持たせる方法の1つとして has_many :through association があります。その際に、中間テーブルの特定の条件に合致したものだけを子レコードとして取り出したい場合があります。

実現方法としては、

  1. 中間テーブルに scope を設定し
  2. 中間テーブルに対して scope 付きの has_many を定義し
  3. 2で定義した has_many を使って、 has_many :through で子テーブルを取得する

という方法が簡単です。

分かってしまえば簡単なのですが、Rails ガイドにも Stack Overflow にもそのものズバリの情報が見つからなかったので、色々試行錯誤してしまいました。参考になれば幸いです。

← 前の投稿

SkyWay vs. Twilio Video

次の投稿 →

BERTについて勉強したことまとめ (1) BERTとは? その特徴と解決しようとした問題、及び予備知識

コメントを残す