背景
モデル関連付けの基礎が抜けていると実感したのでChatGPTと壁打ちしながら復習しました!
備忘録という目的も兼ねて残してまとめます。
当方の執筆前のレベル感
外部キー制約とは?
ポリモーフィック?横文字ワカラナイ....
Railsエンジニア1年目とはいえひどい状態
1対多
例)1人のユーザーが複数のコメントをする関係
rails g model User name:string
rails g model Comment content:text user:references
referencesオプションである、user:references
を付けることでcommentsテーブルに外部キー制約であるforeign_key: true
を追加され、commentsテーブルにはUserのレコードidと紐付くuser_id
のカラムが追加される
# 各モデルファイル
class User < ApplicationRecord
has_many :comments # userは複数の投稿を持つ
end
class Comment < ApplicationRecord
belongs_to :user # commentは特定ユーザーに関連付く
end
# commentsテーブルのマイグレーションファイル
class CreateComments < ActiveRecord::Migration[7.1]
def change
create_table : comments do |t|
t.text :content
t.references :user, null: false, foreign_key: true #外部キー制約
t.timestamps
end
end
end
rails db:migrate
外部キー制約とは
class User < ApplicationRecord
has_many :comments, dependent: :destroy
end
関連付けし合っているモデル(テーブル)同士のデータの整合性を保つ制約。
整合性という観点から、マイグレーションファイルを見ると、null: false
も追加されており、user_id
カラムのNULLが許容されない!つまり、commentに対してuser_id
が必ず紐づくようになっている。
また、存在しないuser_id
(Userモデルのレコードid)を基に、commentレコードを作ろうとすると関連付けがされていないレコードと判断されエラーが起こり作成されない。
dependent: :destroy
外部キー制約で整合性が保たれている場合、user_id
で関連付けされているuserレコードはデフォルトで削除できなくなる。
関連付いているテーブルのレコードは、モデルファイル内でdependent: :destroy
と書き加えることで削除できるようになる。また、その際は関連付くpostデータも削除される。
例)太郎というユーザーのデータを削除すると、太郎のcommentも全て削除される
Railsコンソールで理解を深める
rails c
user = User.create(name: "Taro")
user.comments.create(content: "Nice")
user
=>
id: 1,
name: "Taro",
user.comments
=>
id: 1,
content: "Nice",
user_id: 1,
memo
user.comments.create(content: "Nice")
これは、userに紐づくcommentsを作成している。
commentsが複数形である理由は、Userモデルでhas_many :comments
としたように複数のCommentを所有できるため。
1対1
例)1ユーザーと自身のパスポート
1ユーザーと関連付けさせるためにuser:references
を使用する
rails g model User name:string
rails g model Passport country:string
class User < ApplicationRecord
has_one :passport # has_oneで1対1の関係を定義する
end
class Passport < ApplicationRecord
belongs_to :user
end
# profilesテーブルのマイグレーションファイル
class CreatePassports < ActiveRecord::Migration[7.1]
def change
create_table :passports do |t|
t.string :country
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
rails db:migrate
Railsコンソールで理解を深める
rails c
user = User.create(name: "Taro")
user.create_passport(country: "Japan")
user
=>
id: 1,
name: "Taro",
user.profile
=>
id: 1,
country: "Japan",
user_id: 1,
memo
user.create_passport(country: "Japan")
has_oneで紐付くテーブルを作成する場合、create_テーブル名
もしくはbuild_テーブル名
で作成する必要がある。
build_テーブル名
を使用する場合はDBに作成されずメモリに一時保存されている状況なので、DBに保存する場合はuser.save
も忘れずに!
多対多
例)人間が複数の賃貸と契約するケース、賃貸に複数の人間が契約するケース(同居、ルームシェア)
rails g model Person name:string
rails g model Apartment name:string
Rentalモデルという名の中間テーブルを作成する
rails g model Rental person:references apartment:references
class Person < ApplicationRecord
has_many :rentals # 先に定義する
has_many :Apartments, through: :rentals
end
class Apartment < ApplicationRecord
has_many :rentals # 先に定義する
has_many :persons, through: :rentals
end
# 中間テーブル
class Rental < ApplicationRecord
belongs_to :person
belongs_to :apartment
end
注意点
上記のように、has_many :rentals
中間テーブルを先に定義する必要がある。
これはRailsが中間テーブルの存在を先に理解する必要があるためであり、この順序を守らないと以下のエラー出て怒られてしまう。
ActiveRecord::HasManyThroughOrderError
# rentalsテーブルのマイグレーションファイル
class CreateRentals < ActiveRecord::Migration[7.1]
def change
create_table :rentals do |t|
t.references :person, null: false, foreign_key: true
t.references :apartment, null: false, foreign_key: true
t.timestamps
end
end
end
Rentalsモデルの中間テーブルには、person_id
とapartment_id
のカラムが追加されそれぞれの関連付けができるようになっている。
# スキーマファイルで確認してみる
create_table "participations", force: :cascade do |t|
t.integer "person_id", null: false # これ
t.integer "apartment_id", null: false # これ
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["apartment_id"], name: "index_rentals_on_apartment_id"
t.index ["person_id"], name: "index_rentals_on_person_id"
end
Railsコンソールで理解を深める
rails c
ユーザーとプロジェクトのレコードを作成
person_a = Person.create(name: "Taro")
person_b = Person.create(name: "Takuya")
person_a
=>
id: 1,
name: "Taro"
person_b
=>
id: 2,
name: "Takuya"
apartment_a = Apartment.create(name: "A_apartment")
apartment_b = Apartment.create(name: "B_apartment")
apartment_a
=>
id: 1,
name: "A_apartment",
apartment_b
=>
id: 2,
name: "B_apartment",
person_aに参加するapartment_bを作成し、rentalsテーブルに関連付けのレコードが追加される
person_a.rentals.create(apartment: apartment_b)
person_a.rentals
=>
id: 1,
person_id: 1,
apartment_id: 2,
rentalsテーブル
id | person_id | apartment_id |
---|---|---|
1 | 1 | 2 |
apartment_aに参加するperson_bを作成し、rentalsテーブルに関連付けのレコードが追加される
apartment_a.rentals.create(person: person_b)
apartment_a.rentals
=>
id: 2
person_id: 2,
apartment_id: 1,
rentalsテーブル
id | person_id | apartment_id |
---|---|---|
1 | 1 | 2 |
2 | 2 | 1 |
多対多 - has_and_belongs_to_many
例)人間が複数の賃貸と契約するケース、賃貸に複数の人間が契約するケース(同居、ルームシェア)
rails g model Person name:string
rails g model Apartment name:string
中間テーブルのモデルを作らずに、マイグレーションファイルで中間テーブルのusers_projects
を作成する
rails g migration CreateJoinTablePersonsApartments persons apartments
class Person < ApplicationRecord
has_and_belongs_to_many :apartments
end
class Apartment < ApplicationRecord
has_and_belongs_to_many :persons
end
# マイグレーションファイル
class CreateJoinTablePersonsApartments < ActiveRecord::Migration[7.1]
def change
create_join_table :persons, :projects do |t|
# t.index [:person_id, :apartment_id]
# t.index [:apartment_id, :person_id]
end
end
end
# t.index [:person_id, :apartment_id]
のようにインデックスの追加がコメントアウトの状態でデフォルトで生成されている。
インデックスとは
DB検索を高速化するための仕組み。
前提として、インデックスのないDB検索は、全体を一度スキャンしてヒットした検索結果を取得する。
今回のケースは、person_id
とapartment_id
のペアに対してインデックスが生成され、そのインデックスに紐づくレコードだけ検索されるためクエリの検索効率を加速化させることができる。
じゃあ、全てのレコードに対してインデックスを作成すればよくね?となる気持ちはありますが、インデックスを付けることによって、DBのストレージも消費してしまうので注意が必要となる。
ちなみに!
インデックスを追加した場合、DBの内部構造にインデックス専用のデータ構造が追加されるためテーブルの見た目に変化はない
# スキーマファイル
create_table "apartments_persons", id: false, force: :cascade do |t|
t.integer "person_id", null: false
t.integer "apartment_id", null: false
end
apartments_personsテーブルが作成され、person_id
,apartment_id
で関連付けられるようにしている
Railsコンソールで実践
rails c
ユーザーとプロジェクトのレコードを作成
person_a = Person.create(name: "Taro")
person_a
=>
id: 1,
name: "Taro"
apartment_a = Apartment.create(name: "A_apartment")
apartment_b = Apartment.create(name: "B_apartment")
apartment_a
=>
id: 1,
name: "A_apartment",
apartment_b
=>
id: 2,
name: "B_apartment",
person_aに参加するapartment_bを作成し、apartments_personsテーブルに関連付けのレコードが追加される
person_a.apartments << apartment_b
person_a.apartments
=>
id: 1,
person_id: 1,
apartment_id: 2,
apartments_usersテーブル
id | person_id | apartment_id |
---|---|---|
1 | 1 | 2 |
apartment_aに参加するperson_bを作成し、apartments_personsテーブルに関連付けのレコードが追加される
apartment_a.persons << person_b
apartment_a.persons
=>
id: 2
person_id: 2,
apartment_id: 1,
apartments_usersテーブル
id | person_id | apartment_id |
---|---|---|
1 | 1 | 2 |
2 | 2 | 1 |
ポリモーフィック関連
ポリモーフィック関連を使うと、1モデルが複数のモデルに関連付けできる
例)レビュー(Review)が、映画(Movie)とドラマ(Drama)のそれぞれにできるケース
rails g model Movie title:string
rails g model Drama title:string
複数モデルに関連付くReviewモデルを作成
rails g model Review content:text reviewable:references{polymorphic}
Reviewテーブルに、reviewable_type
とreviewable_id
のカラムが作成される。
reviewable_type
:どのモデルに関連付けするか
reviewable_id
:そのモデルレコードのidを指す
class Movie < ApplicationRecord
has_many :review, as: :reviewable
end
class Drama < ApplicationRecord
has_many :review, as: :reviewable
end
class Review < ApplicationRecord
belongs_to :reviewable, polymorphic: true
end
as:オプションは、ポリモーフィック関連を利用させるために定義するもの。
as: :reviewable
# マイグレーションファイル
class CreateReview < ActiveRecord::Migration[7.1]
def change
create_table :review do |t|
t.text :content
t.references :reviewable, polymorphic: true, null: false # foreign_key: trueがデフォルトで生成されるため削除する
t.timestamps
end
end
end
注意点
rails g model Review content:text reviewable:references{polymorphic}
は、referencesオプションを使っているためマイグレーションファイルにforeign_key: true
が自動で生成される。これを削除せずにdb:migrate
をすると、raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
となり怒られmigrateできない。
ポリモーフィック関連に外部キー制約を付けられない理由
ポリモーフィック関連は、reviewable_type(関連付けするモデル)と reviewable_id(そのモデルレコードのid)を使うことで、複数モデルと関連付けすることができる仕組みに対して、外部キー制約は、特定のテーブルに対して関連付けされる仕組みである。つまりは、複数モデル(テーブル)が対象となるポリモーフィック関連は、特定のテーブルに対して関連づく仕組みである、外部キー制約をつけることができない。
Railsコンソールで実践
rails c
MovieとDramaのレコードを作成
movie = Movie.create(title: "eiga")
image = Drama.create(title: "drama")
movie
=>
id: 1,
title: "eiga",
drama
=>
id: 1,
title: "drama",
movieへのレビューを作成
movie_review = movie.review.create(content: "Greate")
投稿へのレビューを確認
movie.review
=>
id: 1,
content: "Greate",
reviewable_type: "Movie",
reviewable_id: 1,
reviewable_type(reviewの属するポスト)= "Movie"
reviewable_id(そのポストのレコードid)= 1
コメントが付い映画を確認
movie_review.reviewable
=>
id: 1,
title: "eiga!",
dramaへのコメントを作成
drama_review = drama.review.create(content: "Good picture!")
投稿画像へのレビューを確認
drama.review
=>
id: 2,
content: "Good picture!",
reviewable_type: "Drama",
reviewable_id: 1,
レビューが付いたdramaを確認
drama_review.reviewable
=>
id: 1,
title: "drama",