2021年6月25日金曜日

Ruby on Railsにおける多対多モデルの実装

学生が作らんとしているアプリ,入門書ではなかなか触れられていない際どいところに触れてきよったので,補講をした.以下はその記録から要点を抜粋したものである.

やりたいこと

学生たちは自分たちが授業評価をするアプリを作りたいそうだ.みんなのキャンパス学内版,みたいなものか.それでデータ構造を考えている.一人の学生は複数の授業を履修する.ひとつの授業は複数の学生が履修する.したがって,学生テーブルと授業テーブルを作ると,それは多対多の関係ということになる.

ER図はこんな感じ.

モデルの作成

さて,では必要なモデルを作っていこう.まず,学生モデル(Student)を次の操作で作る.

$ bin/rails g model student name:string sid:string

Running via Spring preloader in process 8711

      invoke  active_record

      create    db/migrate/20210625063116_create_students.rb

      create    app/models/student.rb

      invoke    test_unit

      create      test/models/student_test.rb

      create      test/fixtures/students.yml

$ bin/rails db:migrate

== 20210625063116 CreateStudents: migrating ===================================

-- create_table(:students)

   -> 0.0083s

== 20210625063116 CreateStudents: migrated (0.0085s) ==========================


$ 

次に,授業モデル(Lesson)も次の操作で作る.

$ bin/rails g model lesson name:string teacher:string room:string

Running via Spring preloader in process 8788

      invoke  active_record

      create    db/migrate/20210625063223_create_lessons.rb

      create    app/models/lesson.rb

      invoke    test_unit

      create      test/models/lesson_test.rb

      create      test/fixtures/lessons.yml

$ bin/rails db:migrate

== 20210625063223 CreateLessons: migrating ====================================

-- create_table(:lessons)

   -> 0.0052s

== 20210625063223 CreateLessons: migrated (0.0053s) ===========================


$ 

多対多の関係なので,間を取り持つ学生-授業モデル(StudentLesson)を作る.

これがなぜ必要になるのか,なかなか分かりにくいのだが,順繰りに解き明かしていくことにする.とりあえずは,次の操作で学生テーブルと授業テーブルを参照するモデルである StudentLesson を作る.

$ bin/rails g model student_lesson student:references lesson:references

Running via Spring preloader in process 8906

      invoke  active_record

      create    db/migrate/20210625063559_create_student_lessons.rb

      create    app/models/student_lesson.rb

      invoke    test_unit

      create      test/models/student_lesson_test.rb

      create      test/fixtures/student_lessons.yml

$ bin/rails db:migrate

== 20210625063559 CreateStudentLessons: migrating =============================

-- create_table(:student_lessons)

   -> 0.0133s

== 20210625063559 CreateStudentLessons: migrated (0.0133s) ====================


$

モデルクラスの修正

app/models/student_lesson.rb を見てみると,次のようになっている.

class StudentLesson < ApplicationRecord

  belongs_to :student

  belongs_to :lesson

end

学生と授業の間を取り持つクラスはそれぞれに belongs_to しているので,これはそのままこのとおりにしておけばよい.

app/models/student.rb はどうか.ここには,student_lessons を介してlessonsを持つよということを追記する.具体的には次に示す has_many 句の2行を追加する.

class Student < ApplicationRecord

  has_many :student_lessons

  has_many :lessons, through: :student_lessons

end

app/models/lesson.rb にも同様の修正を加える.

class Lesson < ApplicationRecord

  has_many :student_lessons

  has_many :studentsthrough:student_lessons

end


多対多関係の記述,その効果

このようにしておくと,次の操作が可能になる.ActiveRecord の威力を感じられたい.

まず,Studentをひとつ,作る.これを変数 s に入れる.

$ bin/rails c

Running via Spring preloader in process 9093

Loading development environment (Rails 6.1.4)

irb(main):001:0> s = Student.create(name: 'Jun IIO'sid: '21G0123456J')

  TRANSACTION (0.1ms)  BEGIN

  Student Create (5.7ms)  INSERT INTO "students" ("name", "sid", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "Jun IIO"], ["sid", "21G0123456J"], ["created_at", "2021-06-25 06:44:17.699274"], ["updated_at", "2021-06-25 06:44:17.699274"]]

  TRANSACTION (6.1ms)  COMMIT

=> 

#<Student:0x00000001071e0d18

...

次に,Lessonもひとつ,作っておこう.これは変数 l に入れておく.

irb(main):002:0> l = Lesson.create(name: 'Programming I'teacher: 'Taro YAMADA'room: '301')

  TRANSACTION (0.1ms)  BEGIN

  Lesson Create (2.1ms)  INSERT INTO "lessons" ("name", "teacher", "room", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["name", "Programming I"], ["teacher", "Taro YAMADA"], ["room", "301"], ["created_at", "2021-06-25 06:44:56.034435"], ["updated_at", "2021-06-25 06:44:56.034435"]]

  TRANSACTION (0.3ms)  COMMIT

=> 

#<Lesson:0x0000000107228050

...

このようにしておくと,s.lessons << l という操作で,s に l を関連付けることができる(sに紐づけられた lessons に l を追加するという操作である).学生 s が授業 l を受講している状態を表していると解釈することができる.この操作で,StudentLesson のエントリが1つ作られていることがおわかりだろうか?(「StudentLesson Create」とログが出ている)

irb(main):003:0> s.lessons << l

  TRANSACTION (0.2ms)  BEGIN

  StudentLesson Create (6.0ms)  INSERT INTO "student_lessons" ("student_id", "lesson_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["student_id", 1], ["lesson_id", 1], ["created_at", "2021-06-25 06:45:31.907370"], ["updated_at", "2021-06-25 06:45:31.907370"]]

  TRANSACTION (0.3ms)  COMMIT

  Lesson Load (0.3ms)  SELECT "lessons".* FROM "lessons" INNER JOIN "student_lessons" ON "lessons"."id" = "student_lessons"."lesson_id" WHERE "student_lessons"."student_id" = $1  [["student_id", 1]]

=> 

[#<Lesson:0x0000000107228050

  id: 1,

  name: "Programming I",

  teacher: "Taro YAMADA",

  room: "301",

  created_at: Fri, 25 Jun 2021 06:44:56.034435000 UTC +00:00,

  updated_at: Fri, 25 Jun 2021 06:44:56.034435000 UTC +00:00>]

わざわざ中間的な関連を示す中間テーブルを用意した威力は,次の操作で確認することができる.すなわち,今の操作「だけ」で授業に参加する学生の一覧も取ることができるようになるということである.

l.students を表示させてみよう.これは,授業 l を受講している学生の一覧を取得する,という操作である.これまで,授業 l に関しては何の操作もしていない.しかし,次のように受講者の一覧(といってもまだ1人だけだが)を自動的に得ることを,確認できるだろう.

irb(main):005:0> l.students

  Student Load (0.4ms)  SELECT "students".* FROM "students" INNER JOIN "student_lessons" ON "students"."id" = "student_lessons"."student_id" WHERE "student_lessons"."lesson_id" = $1  [["lesson_id", 1]]

=> 

[#<Student:0x000000010702c850

  id: 1,

  name: "Jun IIO",

  sid: "21G0123456J",

  created_at: Fri, 25 Jun 2021 06:44:17.699274000 UTC +00:00,

  updated_at: Fri, 25 Jun 2021 06:44:17.699274000 UTC +00:00>]

irb(main):006:0>

0 件のコメント:

コメントを投稿