ぺんぎんのRails日記

ぺんぎん。エンジニア経験ゼロ。Rails勉強中。

【rails】多対多の関連付け【has_many :through】

モデルの関連付けはそもそもあまり得意じゃないのですが、has_many :throughはもっと苦手です。

ちょっとわかったことをまとめてみます。

1対多の関連付け(belongs_to)

今までやっていたのは以下のような1対他の関連付けでした。

user.rb

has_many :posts

post.rb

belongs_to :user

これはわかりやすいですね。
ユーザーは投稿をたくさん持っていて、投稿は1人のユーザーに従属しています。

問題は次です。

多対多の関連付け

まずは例として、大学生の授業登録のような場面を考えてみたいと思います。

生徒は授業をたくさん持っていて、授業は生徒をたくさん抱えています。

student.rb

has_many :lessons, through: :admin

lesson.rb

has_many :students

(classと区別するために「授業」は"lesson"としました)

これで多対多を表現することができましたが、ここでthroughを使って中間テーブルを入れ込む必要があります。

中間テーブルがない場合

中間テーブル?何それ?おいしいの?って感じですが、これがないと無駄なカラムがたくさん作られてしまうことになるようなんです。

生徒が複数の授業を取っていて、授業も複数の生徒がいるテーブルは以下のようになりますが、一つのカラムに複数の値を入れることはできません。

Image from Gyazo

そこで値ごとにカラムを分けてしまうとこんなふうになります。

Image from Gyazo

気づいた人もいると思いますが、これだと生徒が増えるたびにlessonテーブルのカラムは増えていきます(;∀;)
そして英語や体育のように生徒が少ない授業には、無駄なカラムが増えていくことになります。

すごいムダ٩( ᐛ )و

中間テーブルの設定(has_many :through)

はい、ここからが本題です!
このムダを解消してくれるのが中間テーブルだったんです。

今回は授業登録をしたいので、中間テーブルとしてregistration(登録)テーブルを作ります。

Image from Gyazo

student_id: 1の生徒がlesson_id: 1と2の授業を取っていて、student_id: 2の生徒がlesson_id: 1と3の授業を取っていることを、1つのテーブルにまとめることができました。
こうすれば使わないカラムが作られることはありませんね(^^)

それではhas_many :throughを使ってコードを書いてみましょう。
ちなみに、"through"は「〜を経由して」という意味ですね。
中間テーブルを経由したアソシエーションにしていきます。

student.rb

has_many :registrations
has_many :lessons, through: :registrations

registration.rb

belongs_to :student
belongs_to :lesson

lesson.rb

has_many :registrations
has_many :students, through: :registrations

【ポイント】
ここで気をつけておきたいのが、StudentモデルとLessonモデルでそれぞれhas_many :registrationsをつけているところです。
pikawakaさんのサイトでは「おまじない」として書く決まりになっていると書いてあります。

わたしなりに解釈するなら、「生徒は登録手続きを授業ごとにたくさん行い(student has many registrations)、授業は生徒からの登録をたくさん受け付ける(lessons has many registrations)」という意味合いになるので、どちらにもhas_many :registrationsを付ける、というような感じでしょうか。

has_many :throughでできるようになること

registrationsメソッドを使ってstudentが持っているlessonの値を取得することができるようになっています。

student = Student.first

=> #<Student id: 1, name: "Heiji Hattori", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">

student.lessons

=> [#<Lesson id: 5, subject: "Education", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Lesson id: 2, subject: "Medicine", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Lesson id: 3, subject: "Creative Arts", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">]>

ということで服部平次くんは授業を3つ取っているようですね( ^ω^ )

逆にstudentsメソッドで、どの生徒がその授業を取っているかを見ることもできます。

lesson = Lesson.first

=> #<Lesson id: 1, subject: "Computer Science", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">


lesson.students

=> [#<Student id: 4, name: "Ginshiro Toyama", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Student id: 3, name: "Scar Akai", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">, #<Student id: 2, name: "Yukiko Kudo", created_at: "2021-05-02 07:56:26", updated_at: "2021-05-02 07:56:26">]>

こちらでは、コンピューターサイエンスの授業を取っているのは、遠山さん、赤井さん、工藤のお母ちゃんということがわかりました!

sourceオプション

has_many :throughはsourceオプションをつけることができます。

これは何かというと、中間テーブルを使って取得するカラム(関連付け元)に別名をつけたい時に使うものです。

つまり、さっき上で書いたこれを

has_many :registrations
has_many :lessons, through: :registrations

こんなふうにできます。

has_many :registrations
has_many :registered_lessons, through: :registrations, source: :lesson

どうやらこのsource: :lessonは、中間テーブルregistrationに記述したbelongs_to :lessonを参照してregisterd_lessonsと一致させているようです。

↑の例だとあまり意味がないのですが、たとえば同じモデル内でhas_many :lessonsが重複してしまう時に、区別するために使うといいと思います。

has_many :registrations
has_many :lessons
has_many :registered_lessons, through: :registrations, source: :lesson

ちょっと無理矢理ですが、履修済みのレッスンがhas_many :lessons、登録はしたが未履修のレッスンがhas_many :registered_lessonsといったところでしょうか。

とにかく、sourceオプションは関連元に別名をつけるときに使うものということでした!

参照サイト

railsguides.jp

pikawaka.com

qiita.com

kimuraysp.hatenablog.com