【Rails】関連先のデータを、中間テーブルで絞り込む。

# 目的
多対多のアソシエーションを組んでいて、関連テーブルのデータもincludeなどで取得するとき、
関連テーブル先のデータを中間テーブルのデータで絞り込む方法です。

# 前提
`患者: patient`が`医師: physician`との`診察予約: appointment`をとります。
診察予約が中間テーブルになります。
[Railsガイド](https://railsguides.jp/association_basics.html#has-many-through%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91)を参考にしました。

<details><div>

## migrate

```db/migrate/xxxxxxxxxxxxxx_create_physicians.rb
# 医師
class CreatePhysicians < ActiveRecord::Migration[6.1]
  def change
    create_table :physicians do |t|
      t.string :name

      t.timestamps
    end
  end
end
```
```db/migrate/xxxxxxxxxxxxxx_create_appointments.rb
# 診察予約(中間テーブル)
class CreateAppointments < ActiveRecord::Migration[6.1]
  def change
    create_table :appointments do |t|
      t.belongs_to :physician, null: false, foreign_key: true
      t.belongs_to :patient, null: false, foreign_key: true
      t.datetime :appointment_date

      t.timestamps
    end
  end
end
```
```db/migrate/xxxxxxxxxxxxxx_create_patients.rb
# 患者
class CreatePatients < ActiveRecord::Migration[6.1]
  def change
    create_table :patients do |t|
      t.string :name

      t.timestamps
    end
  end
end
```

`belongs_to`は`references`のエイリアスです。[GitHub rails/…/schema_definitions.rb](https://github.com/rails/rails/blob/ca06a9ff41c2262d1e16e896d754156cb9352179/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L473)

## models

```app/models/physician.rb
class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end
```
```app/models/appointment.rb
class Appointment < ApplicationRecord
  belongs_to :physician
  belongs_to :patient
end
```
```app/models/patient.rb
class Patient < ApplicationRecord
  has_many :appointments
  has_many :physicians, through: :appointments
end
```

## db

**【 Physicians 】**

| id | name |
|:--:|:----:|
| 1 | physician_A |
| 2 | physician_B |

**【 Appointments 】**

| id | physician_id | patient_id | appointment_date |
|:--:|:---:|:---:|:---:|
| 1 | 1 | 1 | 2021/01/10 10:00 |
| 2 | 1 | 2 | 2021/03/30 10:00 |
| 3 | 1 | 3 | 2021/03/20 14:30 |
| 4 | 2 | 2 | 2021/01/20 15:00 |

**【 Patients 】**

| id | name |
|:--:|:----:|
| 1 | patient_01 |
| 2 | patient_02 |
| 3 | patient_03 |

</div></details>

# 方法

```app/models/physician.rb
class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments

  has_many :future_appointments, -> { where(appointment_date: Date.today.. }, class_name: 'Appointment'
  has_many :future_patients, through: :future_appointments, source: :patient
```
```ruby
Physician.preload(:future_patients)
```

### 使用オプション等
|  | 意味 | 参考URL |
|:-:|:--:|:--:|
| [**-> { }**](#スコープ) | スコープ | [Railsガイド](https://railsguides.jp/association_basics.html#has-many%E3%81%AE%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6) |
| **..** | 範囲式 | [リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/Range.html) |
| [**class_name**](#class_name) | 別名使用時のモデル名指定 | [Railsガイド](https://railsguides.jp/association_basics.html#has-many%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-class-name) |
| [**source**](#source) | 別名使用時のモデル名指定 | [Railsガイド](https://railsguides.jp/association_basics.html#has-many%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-source) |


# 解説

取得したいデータは、

- 全ての`医師: physician`のデータ
- 今日以降の`診察予約: appointment`がある、`患者: patient`のデータ

です。

## 失敗例
初め、単純に`where`を繋げて絞り込みをしようとしましたが、失敗でした。

```ruby
Physician.eager_load(:patients).where(appointments: { appointment_date: Date.today..Float::INFINITY})
```

<details><div>

上記の解説をすると、
まず、`eager_load`で関連テーブルのデータも一緒に取得します。
今回は絞り込みも行いたいため、`preload`ではなく`eager_load`を使用しています。`includes`でも結果は同じです。

次に`where`で絞り込みをします。
`where(<テーブル名>: { <カラム名>: <条件> })`で、関連テーブルでの絞り込みができます。[Railsガイド](https://railsguides.jp/active_record_querying.html#%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91%E3%81%AE%E4%B8%80%E6%8B%AC%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%A7%E6%9D%A1%E4%BB%B6%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B)
条件として、`Date.today`で今日の日付を、`..`で範囲指定します。
`Float::INFINITY`は正の数の無限大です。[リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/method/Float/c/INFINITY.html)
`Float::INFINITY`は省略することもできます。`{ appointment_date: Date.today.. }`

しかし、これで発行されるクエリは、

```sql

SELECT `physicians`.`id` AS t0_r0,
       `physicians`.`name` AS t0_r1,
       `physicians`.`created_at` AS t0_r2,
       `physicians`.`updated_at` AS t0_r3,
       `patients`.`id` AS t1_r0,
       `patients`.`name` AS t1_r1,
       `patients`.`created_at` AS t1_r2,
       `patients`.`updated_at` AS t1_r3
FROM `physicians`
LEFT OUTER JOIN `appointments` ON `appointments`.`physician_id` = `physicians`.`id`
LEFT OUTER JOIN `patients` ON `patients`.`id` = `appointments`.`patient_id`
WHERE `appointments`.`appointment_date` >= '2021-02-28'
```
<details><div>

`SELECT`で取得するカラムを指定。`AS`はカラムに別名をつけています。
`FROM`は取得するテーブル名。
`LEFT OUTER JOIN`で同時に取得するテーブルを指定。`ON`で条件付けています。
そして、`WHERE`で今日以降のデータに絞り込んでいます。

</div></details>

となり、**今日以降の診察予約がある、医師と患者のデータ**のみを取得し、**全ての医師のデータ**を取得できません。


```ruby
physician = Physician.eager_load(:patients).where(appointments: { appointment_date: Date.today..Float::INFINITY})

physician
=> [<id: 1, name: "physician_A">]

physician[0].patients
=> [<id: 2, name: "patient_02">,
    <id: 3, name: "patient_03">]
```

</div></details>

## 成功

### スコープ
色々と調べた結果、スコープを使用することでうまくいきました。

```ruby
class <モデル名> < ApplicationRecord
  has_many :<関連モデル名>, -> { <クエリメソッド> }
end
```

スコープを使用することで、モデルの関連モデルとして呼び出したとき、クエリメソッドが実行されます。

例えば、`著者: author`と`書籍: book`が1対多の関係であったとき、

```models/author.rb
class Author < ApplicationRecord
  has_many :books, -> { order(:publication_date) }
end
```

とすることで、

```ruby
author.books
```
としたとき、`書籍: book`を`出版日: publication_date`の順番で取得することができます。

<br>

これを利用して多対多のアソシエーションの中間テーブルでの絞り込みを行います。
今回は中間テーブルである`診察予約: appointment`の日付でクエリメソッド`where`を使用したいため、

```app/models/physician.rb
class Physician < ApplicationRecord
  has_many :appointments, -> { where(appointments_date: Date.today..) }
  has_many :patients, through: :appointments
end
```

となります。

`Date.today`で今日の日付を取得し、`..`で<span id='range'>範囲指定</span>。末端を指定しないことで、以降全てが範囲となります。
(`..`の末端未指定は`Ruby 2.6.0以降`でのみ使用可能です。それ以前のバージョンの場合は、`Float::INFINITY`を指定してください。[リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/method/Float/c/INFINITY.html))

データの取得は、

```ruby
Physician.preload(:patients)
```

と記述します。

今回は、`eager_load`ではなく`preload`を使用しています。
`where`による絞り込みを`appointments`テーブル単体で行い、`joins`などで他テーブルを参照する必要がないためです。
これは`includes`でも同じ結果となります。

このとき発行されるクエリは、

```sql
Physician Load (0.8ms)  SELECT `physicians`.* FROM `physicians`
Appointment Load (0.8ms)  SELECT `appointments`.* FROM `appointments` WHERE `appointments`.`appointment_date` >= '2021-02-28' AND `appointments`.`physician_id` IN (1, 2)
Patient Load (0.5ms)  SELECT `patients`.* FROM `patients` WHERE `patients`.`id` IN (2, 3)
```

となります。

1行目で`医師: physician`のデータを取得。`SELECT`で`physicians`の全てのカラム`*`を取得しています。`FROM`はテーブルの指定です。

2行目は`診察予約: appointment`のデータを取得しています。これも同じく`SELECT`で全てのカラム`*`を取得していますが、`WHERE`で絞り込みを行っています。
`` `appointments`.appointment_date` >= '2021-02-28' ``は、`app/models/physician.rb`に記載したスコープのクエリメソッドになります。
さらに、`AND`で`` `appointments`.`physician_id` IN (1, 2) ``とすることで、1行目で取得した`医師: physician`の`id`に関連するデータのみを取得しています。

最後に、3行目で`患者: patient`のデータ取得です。`WHERE`で`` `patients`.`id` IN (2, 3) ``と条件付けすることにより、`patients`の`id`が、2行目で取得した`診察予約: appointment`に関連するデータのみを取得しています。

これで、全ての`医師: physician`と、本日以降の`診察予約: appointment`がある`患者: patient`のデータを取得することができます。

```ruby
physician = Physician.preload(:patients)

physician
=> [<id: 1, name: "physician_A">,
    <id: 2, name: "physician_B">]

physician[0].patients
=> [<id: 2, name: "patient_02">,
    <id: 3, name: "patient_03">]

physician[1].patients
=> []
```

しかし、今の状態では、アソシエーションで呼び出す全ての場合にクエリメソッドが効いてしまいます。
つまり、今日(2021/02/28)以前のデータをアソシエーションで呼び出すことができない状態です。

そのため、モデルの別名を作成し、絞り込みの際はそちらを使用することにします。

### class_name

`class_name`は、モデルを別名で使用する際、どのモデルのかを指定することができます。

```ruby
class モデル名 < ApplicationRecord
  has_many :関連モデルの別名, class_name: '関連モデル名'
end
```

例えば、`筆者: author`と1対多である`書籍: book`があり、
`書籍: books`テーブルの`commic`カラムが`true`のとき、漫画であるとすると、

```app/models/author.rb
class Author < ApplicationRecord
  has_many :comics, -> { where(commic: true) }, class_name: 'Book'
end
```

と、モデルに記述することで、

```ruby
author.commics
```

のように、モデルの別名を使用して、`書籍: books`テーブルの中の漫画のみをアソシエーションで取得することができます。
`class_name`のモデル名は**文字列指定・頭大文字・単数形**であることに注意してください。

<br>

しかし、今回の場合は多対多のアソシエーションを使用しています。
中間テーブルには`class_name`のオプションでいいのですが、アソシエーション先で`through`を使用しているとうまくいきません。

### source

アソシエーションで`through`を使用している場合、別名をつける際は`class_name`の代わりに、`source`を使用します。

```ruby
class モデル名 < ApplicationRecord
  has_many :中間テーブルのモデルの別名, class_name: '中間テーブルのモデル名'
  has_many :関連モデルの別名, through: :中間テーブルのモデルの別名, source: :関連モデル名
end
```

今回のように、別名の中間テーブルのモデルを使用する場合、`through`の名前も別名を指定しなければなりません。
また、`class_name`のときとは違い、`source`では、**シンボル指定・頭小文字・単数形**で指定しています。単数形であることは変わりません。

よって、

```app/models/physician.rb
class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments

  has_many :future_appointments, -> { where(appointment_date: Date.today..) }, class_name: 'Appointment'
  has_many :future_patients, through: :future_appointments, source: :patient
end
```

と記述し、
<br>

```ruby
# 全て
physician.preload(:patients)

# 今日以降
physician.preload(:future_patients)

```
のように、使い分けることができます。

## 注意点: 引数は使用できません。

色々と調べたのですが、引数の使用はできないようでした。
`Date.today`や`true` `false`など、値が決まっていたらhas_manyのスコープが使えるのですが、`where`で日付を指定するなどはできませんでした。

```ruby
# これはできません。
class Author < ApplicationRecord
  has_many :books, ->(date) { where(created_at: date) }
end
```
<br>

引数?として使用できるのはそのモデルのデータだけのようです。
[GitHub rails/…/associations.rb](https://github.com/rails/rails/blob/main/activerecord/lib/active_record/associations.rb#L595-L603)


# 最後に

言い回しや解釈の間違っているところ、さらに良い方法がございましたら、ご教示いただけますと幸です。
ご覧いただき、ありがとうございました。