0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsモデルの関連付けについて

Posted at

背景

モデル関連付けの基礎が抜けていると実感したので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_idapartment_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_idapartment_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_typereviewable_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",
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?