モデルの多対多参照

 いくつかの選択授業があるとします。各学生は0個~n個の授業を選び,各授業には0人~m人の学生が所属しています。このような関係が「多対多」です。

class Lesson < ApplicationRecord
  has_and_belongs_to_many :students
end

class Student < ApplicationRecord
  has_and_belongs_to_many :lessons
end

 モデルの作成にはrailsコマンドを使います。なお,rails grails generateの省略形です。

$ rails g model Student
$ rails g model Lesson

 モデルを作成するのと同時に,データベースのテーブルも作成する必要があります。既存または新規のマイグレーションにコードを追加します。
 必要となるテーブルは,学生のレコードと授業のレコードを結びつけるための,中間的なテーブルです。1対1の関係であれば,それぞれのモデルに参照用のカラムを追加すれば済みますが,多対多の関係ではそれでは済みません。

class CreateLessonsStudents < ActiveRecord::Migration[5.0]
  def change
    create_table :lessons_students, id: false do |t|
      t.belongs_to :lesson
      t.belongs_to :students
    end
  end
end

 マイグレーションを作成する場合はrailsコマンドを使います。

$ rails g migration CreateLessonsStudents

参照を追加するためのフォーム

 scaffoldを作成すると,ビューの中に_form.html.erbというファイルが作成されています。これは,オブジェクトのデータを入力するためのフォームを生成するための,パーシャルと呼ばれるコードの断片です。

 一つの電話番号に,一人の学生が関連づけられている場合を考えてみます。

class Phone < ApplicationRecord
  belongs_to :student
end

class CreatePhones < ActiveRecord::Migration[5.0]
  def change
    create_table :phones do |t|
      t.string :number
      t.references :student, foreign_key: true

      t.timestamps
    end
  end
end

 scaffoldで作成された_form.html.erbは次のようになっており,参照する学生のIDを直接入力させるようになっています。

<%= form_for(test_phone) do |f| %>
  <% if test_phone.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(test_phone.errors.count, "error") %> prohibited this test_phone from being saved:</h2>

      <ul>
      <% test_phone.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :number %>
    <%= f.text_field :number %>
  </div>

  <div class="field">
    <%= f.label :student_id %>
    <%= f.text_field :student_id %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

 これでは何番のIDが何という学生に対応するのか分からないため,学生の一覧をリストボックスで表示して,そこから選ぶような形に変更します。

  <div class="field">
    <%= f.label :student_id %>
    <%= f.collection_select :student_id, Student.all, :id, :name %>
  </div>

追加されたカラムに関する保存処理

 モデルを作成した後で,対応するテーブルにカラムを追加したくなる場合があります。このとき,コントローラーを適切に修正しなければ,データが全く保存されないという事態に陥る可能性があります。

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_phone
      @phone = Phone.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def phone_params
      params.require(:phone).permit(:number)
    end

 メソッドphone_paramsでは,いわゆるホワイトリスト処理を行い,許可された名前のパラメータ以外は受け付けないようにしています。そのため,追加したカラムの名前をホワイトリストに加えなければ,何をしてもデータが変更されないということになってしまいます。
 学生への参照を追加した場合は,以下のように変更します。

    def phone_params
      params.require(:phone).permit(:number, :student_id)
    end

データベース初期値の設定

 db/seeds.rbにコードを記述することで,データベースの初期値を設定できます。

students = Student.create([{name: "Tom"}, {name: "Mary"}, {name: "John"}])

lesson = Lesson.new(name: "math")
lesson.students = [students[0], students[2]]
lesson.save

lesson = Lesson.new(name: "science")
lesson.students = [students[1]]
lesson.save

 リテラルデータのほか,オブジェクト同士の関連を設定することもできます。
 newで作成した場合は,saveメソッドを呼ばなければデータベースに記録されないので注意が必要です。createで作成した場合は,そのままデータベースに記録されます。関連を設定する場合は,オブジェクト名_idにオブジェクトのidを直接設定することになります。