【Rails】簡単な事例で学ぶモデルの多対多関連付け(アソシエーション)の書き方
ここでは Rails においてテーブル同士の多対多の関連付け(アソシエーション)の書き方について実験していきます。事例を通じて実際の動き方を確かめてみましょう。
【Rails】簡単な事例で学ぶモデルの一対多関連付け(アソシエーション)の書き方と併せて読むと,より理解が深まるでしょう。
(この記事では,実際のコードをコントローラー内で書くことを想定して変数に@を付けていますが,本来コンソール上で動かす際には必要ありません。)
英会話スクールの予約モデル
英会話スクールの予約を管理するモデルを考えてみます。
ここでは4つのテーブルが登場します。テーブル lecturer には2名の講師がいます。また,テーブル lecture にはどの講師が何時に授業を行うかを表すデータが入っています。lecturer_id=1 は田中先生を指し,10:00に授業を行います。また lecturer_id=2 は佐藤先生を指し,15:00に授業を行います。
このときテーブル lecture を参照元,テーブル lecturer を参照先を呼びます。カラムの命名にはルールがあり,参照元には参照先のテーブル名_idというカラムが必要です。
また,テーブル student には2名の生徒がいます。
最終的に講師と生徒をつなぐテーブルが appointment です。レコードは田中先生の10:00に野口さんが予約したことを意味しています。
このモデルは複数の講師と複数の生徒を関連付けるので,多対多関連付け(アソシエーション)と呼びます。そして,両者の間にテーブル appointment があり,講師と生徒を結びつける役割を果たしています。このようなテーブルを中間テーブルと呼びます。多対多のモデルでは一般的に中間テーブルを必要とします。
考察1
ここで lecturer と student が多対多の関係であるという言い方はやや正確さを欠きます。上のモデルで実際に中間テーブルを介して結びついているのは lecture と student だからです。
appointment 側から見ると lecture は具象であり直接的な関係,lecturerは抽象であり間接的な関係です。
モデルを構築するときには,このように直接結びついているもの,間接的に結びついているものを頭の中で整理しておくと,具体的なコードの書き方がひらめくようになるでしょう。
テーブルの作成
テーブルを作成していきましょう。
> rails g model lecturer name:string
> rails g model student name:string
> rails rails g model lecture lecturer_id:integer time:string
> rails g model appointment lecture_id:integer student_id:integer
関連付けを行いましょう。とりあえず,はじめの図に沿って以下のように書いてみます。
class Lecturer < ApplicationRecord
has_many :lectures, dependent: :destroy
end
class Lecture < ApplicationRecord
has_many :appointments, dependent: :destroy
end
class Student < ApplicationRecord
has_many :appointments, dependent: :destroy
end
class Appointment < ApplicationRecord
belongs_to :lecture
belongs_to :student
end
> rails db:migrate
Railsコンソールを立ち上げて,lecturer のレコードを追加します。
>> Lecturer.create(name:"田中")
>> Lecturer.create(name:"佐藤")
lecture のレコードを追加します。
>> @lecturer=Lecturer.find_by(name:"田中")
>> @lecturer.lectures.create(time:"10:00")
=> id: 1, lecturer_id: 1, time: "10:00"
本来,時刻を扱う場合はDATETIME型のカラムにしますが,ここでは話を簡単にするため,単に文字列として記述しています。
上のような書き方をすることで,レコードに lecturer_id が自動的に挿入されていることが分かります。
他のレコードも追加しましょう。
>> @lecturer=Lecturer.find_by(name:"佐藤")
>> @lecturer.lectures.create(time:"15:00")
>> Lecture.all
=> id: 1, lecturer_id: 1, time: "10:00"
id: 2, lecturer_id: 2, time: "15:00"
>> Student.create(name:"秋山")
>> Student.create(name:"野口")
>> Student.all
=> id: 1, name: "秋山",
id: 2, name: "野口"
予約する
野口さんが10:00に予約を入れるという動作をコードで表現してみましょう。
>> @student=Student.find_by(name:"野口")
>> @lecture=Lecture.find_by(time:"10:00")
>> @lecture.students=@student
/usr/local/bundle/gems/activemodel-6.1.5/lib/active_model/attribute_methods.rb:469:in `method_missing': undefined method `students=' for ・・・
エラーが発生しました。モデルの書き方に問題があるようです。
student は lecture に直接に属しているわけではないので,@lecture.students という書き方はできません。両者の間には appointment が存在します。
モデルの修正
class Lecturer < ApplicationRecord
has_many :lecture, dependent: :destroy
end
class Lecture < ApplicationRecord
has_many :appointments, dependent: :destroy
has_many :students, through: :appointments
end
class Student < ApplicationRecord
has_many :appointments, dependent: :destroy
has_many :lectures, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :lecture
belongs_to :student
end
問題を解決するために lecture と student に through を追加しました。これは lecture が appointments を通じて間接的に student を持つことを意味します。
もう一度予約を入れてみましょう。
>> @student=Student.find_by(name:"野口")
>> @lecture=Lecture.find_by(time:"10:00")
>> @lecture.students<<@student
>>Appointment.all
=> id: 1, lecture_id: 1, student_id: 2
今度はうまくいきました。
ここで << という書き方が登場しています。これは配列に要素を追加するもので配列演算子と呼びます。文字数が少なくて済むので使用していますが,push()を使っても同じことができます。
>> @lecture.students.push(@student)
has_many を設定することで lecture は複数の student を持つことができるようになり,どの student が属しているかは @lecture.students の中に配列として格納されます(はじめは空の配列の状態です)。そこに <<@student とすることで新たな student を追加することができます。
このとき,中間テーブル appointment にレコードが自動的に追加される仕組みです。
不正なデータ
仮に,レコードに存在しない student を登録しようとしたらどうなるか,という実験をしてみましょう。
>> Appointment.all
=> id: 1, lecture_id: 1, student_id: 2
>> @appointment=Appointment.id(1)
>> @appointment[:student_id]=3
>> @appointment
=> id: 1, lecture_id: 1, student_id: 3
テーブルには id が 3 の student は存在しないのですが,強引にデータを改変してみました。これを保存してみましょう。
>> @appointment.save
・・・
TRANSACTION (0.2ms) ROLLBACK
=> false
保存に失敗しました。データをもとに戻してみましょう。
>> @appointment[:student_id]=2
>> @appointment.save
・・・
TRANSACTION (0.2ms) COMMIT
=> true
今度は成功しました。
このように,テーブルの関連付けを行うことで不正なデータの入力を防ぐことができます。
考察2
先ほど予約を入れるときに,@lecture.students<<@student という書き方をしました。これは一つの lecture は複数の student を持つ,という解釈です。ここには疑問点があります。
この考え方を用いて,一つの lecture がどの student を持っているかを調べることはできるのですが,逆に一人の student がどの lecture を持っているかを調べるにはどのようにしたらよいでしょうか。
>> @lecture
=> id: 1, lecturer_id: 1, time: "10:00"
>> @student
=> id: 2, name: "野口"
>> @lecture.students
=> id: 2, name: "野口"
>> @student.lectures
=> id: 1, lecturer_id: 1, time: "10:00"
答えは,書き方をひっくり返して @student.lectures とすることです。
このように,lecture と student の両方に has_many ~ through ・・・ を設定することで,どちら側からでも情報を取得することができます。
まとめ
ここでは英会話スクールの予約モデルを通じて,多対多の関連付けについて学びました。ポイントは has_many ~ through ・・・を用いて,2つのテーブルを中間テーブルを通して関連付けることです。
関連付けを行うことで,中間テーブルの存在を意識せずに2つのテーブルに存在するレコード同士を結びつけ,また不正なデータが中間テーブルに登録されることを防止できます。
SNSでシェア