【Rails】簡単な事例で学ぶモデルの一対多関連付け(アソシエーション)の書き方

Rails ではテーブル同士を関連付けることができるのですが,実際どのようにコードを書けばよいのか最初は分かりにくいものです。ここでは一対多の関連付けを簡単な事例で試して,実際にどのように動くのかを見ていきます。

【Rails】簡単な事例で学ぶモデルの多対多関連付け(アソシエーション)の書き方も併せてお読みください。

テーブルの作成

まずはテーブルを作成しましょう。

> rails g model classroom class_name:string
> rails g model student student_name:string

教室 classroom と生徒 student のテーブルを作りました。

> rails db:migrate

テーブルを関連付ける

それぞれのクラスが複数の生徒を持つようにテーブルを関連付けていきます。

class Classroom < ApplicationRecord
  has_many :students, dependent: :destroy
end
class Student < ApplicationRecord
  belongs_to :classroom
end

classroom は複数の生徒を持つので students複数形で書きます。一方,students は一つの classroom に所属するので classroom と単数形で書きます。

レコードの追加

rails c でRailsコンソールを立ち上げ,レコードを追加していきます。

>> Classroom.create(class_name: "1組")
>> Classroom.create(class_name: "2組")

レコードを確認しましょう。

>> Classroom.all
=> id: 1, class_name: "1組",
   id: 2, class_name: "2組",

次に,生徒のレコードを作成します。

>> @classroom=Classroom.find_by(id:1)

>> @student=@classroom.students.create(student_name:"田中")

/usr/local/bundle/gems/activemodel-6.1.4.6/lib/active_model/attribute_assignment.rb:51:in `_assign_attribute': unknown attribute 'classroom_id' for Student. (ActiveModel::UnknownAttributeError)

エラーが発生しました。テーブル Student に classroom_id というカラムが無いとのことです。

カラムを追加する

カラムを追加してみましょう。

> rails g migration add_column_to_student
class AddColumnToStudent < ActiveRecord::Migration[6.1]
  def change
    add_column :students, :classroom_id, :integer
  end
end
> rails db:migrate

Railsコンソールを立ち上げて

>> @classroom=Classroom.find_by(id:1)
>> @student=@classroom.students.create(student_name:"田中")
=> [["student_name", "田中"], ・・・ ["classroom_id", 1]]

今度はうまくいきました。もう一人,生徒を追加してみます。

>> @classroom.students.create(student_name:"佐藤")
=> [["student_name", "佐藤"], ・・・ ["classroom_id", 1]]

classroom_id に先ほど同様,1 が入っています。

さらに,別のクラスを作って生徒を追加してみましょう。

>> @classroom=Classroom.create(class_name:"2組")
>> Classroom.all
=> id: 1, class_name: "1組",
   id: 34, class_name: "2組",

2組の id が 34 になっています。id の数字はRailsが自動で割り当てるものなので,こちらの思い通りにならないことがあります。もし,1組に 1,2組に 2 を割り当てたいという場合は,別のカラムを追加して値を格納すると良いでしょう。

生徒を追加します。

>> @classroom.students.create(student_name:"林田")
=> [["student_name", "林田"], ・・・["classroom_id", 34]]

>> @classroom.students.create(student_name:"秋山")
=> [["student_name", "秋山"], ・・・ ["classroom_id", 34]]

同じように classroom_id が 34 になっています。

レコードの抽出

ここで,1組の生徒だけを抽出してみましょう。

>> @classroom=Classroom.find_by(class_name:"1組")
>> @classroom.students
=> id: 1, student_name: "田中", classroom_id: 1>,
   id: 2, student_name: "佐藤", classroom_id: 1>,

1組の生徒を抽出することができました。

検証その1

一対多のテーブルを作成した時,テーブル classroom を参照先,テーブル student を参照元と呼びます。

はじめに生徒のレコードの追加に失敗したのは classroom_id というカラムが存在しなかったからです。このように参照元のテーブルは参照先のテーブル名_idというカラムを必要とします。そしてレコードを追加したときにそのカラムには参照先のidが自動で書き込まれます。

実際にテーブルを操作する上において id の値がいくつであるかを気にする必要はありません。試しに1組に生徒を追加してみましょう。

>> @classroom=Classroom.find_by(class_name:"1組")

>> @classroom.students.create(student_name:"野口")
=> [["student_name", "野口"], ・・・ ["classroom_id", 1]]

このように find_by を使うことで,id の代わりに class_name を指定して生徒を追加できました。ただし,これは class_name のデータに重複がないことが前提です。

レコードを削除する

これまでに作成した生徒のレコードを確認しておきましょう。

>> Student.all
=>id: 1, student_name: "田中", classroom_id: 1,
  id: 2, student_name: "佐藤", classroom_id: 1,
  id: 3, student_name: "林田", classroom_id: 34,
  id: 4, student_name: "秋山", classroom_id: 34,
  id: 5, student_name: "野口", classroom_id: 1

1組を削除してみます。

>> @classroom=Classroom.find_by(class_name:"1組")
>> @classroom.delete
>> Classroom.all
=> id: 34, class_name: "2組"

1組が消え,2組だけが残っていることが確認できました。

ここで生徒のテーブルを確認してみます。

>> Student.all
=> id: 1, student_name: "田中", classroom_id: 1,
   id: 2, student_name: "佐藤", classroom_id: 1,
   id: 3, student_name: "林田", classroom_id: 34,
   id: 4, student_name: "秋山", classroom_id: 34,
   id: 5, student_name: "野口", classroom_id: 1

生徒のレコードに変化はありません。

今度は delete ではなく destroy を使ってみます。

>> @classroom.destroy
>> Student.all
=> id: 3, student_name: "林田", classroom_id: 34,
   id: 4, student_name: "秋山", classroom_id: 34

今度は1組の生徒が削除されました。

検証その2

一対多の関連付けを行うとき,参照先のレコードを destroy で削除すると,それに関連する参照元のレコードも自動的に削除されます。

これを用いて,たとえば twitter のようなアプリなら,ユーザーが退会したときにそのユーザーのつぶやきをすべて削除することができるでしょう。

孫テーブルを作る

一対多の関連付けができたところで,さらに孫テーブルを作ってみましょう。その前に,テーブルのレコードをいったんリセットしておきます。

> rails db:reset

孫テーブルを作り,生徒に国語と数学の得点を関連付けます。得点を記録するためにテーブル score を作成します。

> rails g model score student_id:integer subject:string score:integer

関連付けを行うには,参照先のテーブル名_idというカラムが必要だったことを思い出しましょう。ここではテーブル student と関連付けるので student_id というカラムを用意しています。

テーブルを関連付けましょう。

class Classroom < ApplicationRecord
  has_many :students, dependent: :destroy
end

class Student < ApplicationRecord
  belongs_to :classroom
  has_many :scores, dependent: :destroy
end

class Score < ApplicationRecord
  belongs_to :student
end
> rails db:migrate

Railsコンソールに移動して,レコードを追加します。

>> @classroom=Classroom.create(class_name:"1組")
>> @student=@classroom.students.create(student_name:"田中")
>> @student.scores.create(subject:"国語", score:85)
>> @student.scores.create(subject:"数学", score:90)

レコードを確認します。

>> Score.all
=> id: 1, student_id: "1", subject: "国語", score: 85,
   id: 2, student_id: "1", subject: "数学", score: 90

さらに他の生徒を作り,得点を追加します。

>> @student=@classroom.students.create(student_name:"佐藤")
>> @student.scores.create(subject:"国語",score:80)
>> @student.scores.create(subject:"数学",score:70)
>> Score.all
=> id: 1, student_id: "1", subject: "国語", score: 85,
   id: 2, student_id: "1", subject: "数学", score: 90,
   id: 3, student_id: "2", subject: "国語", score: 80,
   id: 4, student_id: "2", subject: "数学", score: 70

クラスをもう一つ作ります。

>> @classroom=Classroom.create(class_name:"2組")
>> @student=@classroom.students.create(student_name:"小川")
>> @student.scores.create(subject:"国語",score:100)
>> Score.all
=> id: 1, student_id: "1", subject: "国語", score: 85,
   id: 2, student_id: "1", subject: "数学", score: 90,
   id: 3, student_id: "2", subject: "国語", score: 80,
   id: 4, student_id: "2", subject: "数学", score: 70,
   id: 5, student_id: "3", subject: "国語", score: 100

ここで,1組を削除してみます。

>> @classroom=Classroom.find_by(class_name:"1組")
>> @classroom.destroy
>> Student.all
=> id: 3, student_name: "小川", classroom_id: 2

>> Score.all
=> id: 5, student_id: "3", subject: "国語", score: 100

1組を削除すると,1組に所属する生徒だけでなく得点も当時に削除されていることが確認できます。

まとめ

ここではクラスと生徒を一対多で関連付けるテーブルを作成しました。このとき一の側(クラス)を参照先,多の側(生徒)を参照元と呼びます。

今回のポイントは以下です。

・参照先に has_many,参照元に belongs_to を設定。

・参照元に参照先のテーブル名_idというカラムを用意する。

・参照先のレコードをdestroyしたら,参照元の関連するレコードも削除される。

実際のコードはコントローラーに書いていくことになりますが,上で紹介したコードの書き方を参考にしてみてください。