多くのWebアプリで一般的に利用されているリレーショナルデータベースでは、複数のテーブルを関連付けることができます。
Ruby on RailsのORMであるActive Recordにも、モデル同士のリレーションを表現するためのアソシエーションという機能があります。

アソシエーションを使いこなせば、あるモデルから関連する別のモデルへアクセスが可能になり、アプリケーション内でのデータの扱いがわかりやすくなります。

オンラインでプログラミング学習を始めてみたいなら、米国シリコンバレー発祥の世界最大級のオンライン学習プラットフォーム、Udemyがおすすめです。
こちらの記事で紹介するフレームワーク、Ruby on Railsも学ぶことができます!
よくわかるRuby on Rails入門 オンライン講座

アソシエーションとは

アソシエーションを設定することで、複数のモデル間の関係性(リレーション)を定義して、互いに参照できるようになります。
あるモデルから別のモデルのレコードを参照したり、あるレコードの登録時に関連付けたレコードを自動登録したりといったことができるようになります。
アソシエーションを身につけることで、アプリケーション開発が非常に便利になりますので、ぜひマスターしておきましょう。

今回は、イベントを管理するというサンプルアプリを参考にアソシエーションを設定していきます。
同じ環境で試してみたい方は、以下に従ってコマンドを実行しておいてください。
※ generateコマンドにはgコマンドという省略形もあります。
※ generateコマンドを打ち間違えた場合はdestroyコマンドでやり直せます。

# 新規プロジェクトの作成
$ rails new event-app
$ cd event-app

# scaffoldコマンドで各種モデルの関連ファイルを自動生成
$ rails generate scaffold event name:string hold_date:date
$ rails generate scaffold ticket fee:integer
$ rails generate scaffold venue name:string capacity:integer
$ rails generate scaffold guest name:string

# データベースの作成
$ rails db:create

# マイグレーションの実行
$ rails db:migrate

仕様は以下に示した通りで、括弧内は対応するモデル名です。

No関係性アソシエーションの種類
1ひとつのイベント(Event)に対してひとつのチケット(Ticket)が対応する 一対一
2ひとつの会場(Venue)は複数のイベント(Event)を管理できる 一対多
3ひとつのイベント(Event)に対して複数人のゲスト(Guest)が参加できる
また、ゲスト(Guest)は複数のイベント(Event)に参加できる
多対多

現場を意識した本格的な学習カリキュラムで、本物のスキルを身につけたいならこちら!
困ったときにいつでも相談できるバディ制度でわからないこともすぐに解決できます。
プログラミングスクールといえば【RUNTEQ】

主キーと外部キー

データベースのテーブル同士のリレーションを学ぶには、主キーと外部キーの知識が必須となります。
どちらもデータベースの基本的な概念になりますので、しっかり理解しておきましょう。

主キーについて

主キー(primary key)とは、テーブルのレコードを一意に表すためのカラムのことです。
通常は「id」というカラム名で、最初に登録されたレコードから順番に番号を振っていきます。
これは出席番号のようなもので、必ずユニークな(重複しない)値である必要があります。

例えば、ある学校(データベース)のあるクラス(テーブル)の生徒たち(レコード)について考えてみましょう。
そのクラスの「山田花子」さんを探したいとしますが、同姓同名の生徒が2名以上いる場合に「氏名」ではその生徒を見つけるができません。
つまり「氏名」というカラムはユニークではないということになり(重複したレコードが2つ以上あるので)、主キーとは言えません。
同様に「性別」や「身長」といったカラムも主キーにはなりえません。
しかし「出席番号」というカラムは他の生徒とかぶることがないため、これは主キーであるといえます。

『出席番号12番の生徒はどこですか?』と聞けば、必ず条件に合った1名の生徒を見つけることができます。
データベースがやっていることはこれと同じで、このような質問のことをクエリといいます。

外部キーについて

外部キー(foreign key)とは、あるテーブルの主キーとして別のテーブルのユニークなカラムを利用するというものになります。

例えば先ほどの学校の例で、あるテストが行われたとします。
このテストの結果がひとつのテーブルとして扱われているとき、そのレコードはそれぞれ生徒に紐づいていることになります。
つまり「生徒」テーブルと「テスト」テーブルの間に関係性(リレーション)があるのです。
この場合は、テストのレコードを一意に取得する際に、生徒の出席番号を用いると便利です。

『出席番号21番のテスト結果はどうですか?』と聞けば、「生徒」テーブルの主キーである「出席番号」というカラムを使って、「テスト」テーブルのレコードが取得できます。
これが外部キーです。

一対一、一対多のアソシエーション

外部キー制約の追加

外部キー制約とは、あるテーブルのカラムに対して他のテーブルの特定カラムに存在する値のみが使用できるという制約です。
つまり、外部のテーブルを参照することが保証されます。

作成済みのticketsテーブルに対してeventのIDを参照するカラムを追加してみます。
※すでに存在するマイグレーションファイルは書き換えないように注意してください。

ticketsテーブルの定義を更新するマイグレーションファイルを作成しましょう。
また、外部キーを参照するカラムは必ず値が存在しなくては意味がありませんので、not null制約も追加します。

$ rails generate migration AddEventIdToTickets
class AddEventIdToTickets < ActiveRecord::Migration[6.1]
  def change
    # 外部キーの設定
    add_reference :tickets, :event, foreign_key: true
    # not null制約の設定
    change_column :tickets, :event_id, :integer, null: false
  end
end

同様にして、eventsテーブルに対してvenueのIDを参照するカラムも追加しておきましょう。

$ rails generate migration AddVenueIdToEvents
class AddVenueIdToEvents < ActiveRecord::Migration[6.1]
  def change
    # 外部キーの設定
    add_reference :events, :venue, foreign_key: true
    # not null制約の設定
    change_column :events, :venue_id, :integer, null: false
  end

ここまできたらマイグレーションを実行したいのですが、注意が必要です。
既存のデータが存在する場合、外部キー制約との間に不整合性が生まれて例外が発生する可能性があるからです。

このような不具合の原因は、既存のテーブルに新しくnot null制約のついたカラムを追加しようとしているのに、今までのデータにはデータがないのでnullになってしまうことです。
以下のようなActiveRecord::NotNullviolationの例外が発生します。

ActiveRecord::NotNullViolation: SQLite3::ConstraintException: NOT NULL constraint failed: events.ticket_id

その場合は、いったんデータベースをリセットしてからマイグレーションを実行しましょう(他には、直接SQLを発行して不整合性を解決する方法もあります)。
データベースをリセットするコマンドはrails db:resetです。

Windowsでsqlite3を用いている環境ではdb:resetが使えません。
Railsのバグによりエラーが発生してしまうので、データベースを直接削除しましょう。

# Windows以外の場合(Windows環境ではPermissionエラーが発生する)
$ rails db:reset

# Windowsでsqlite3を用いている場合はデータベースファイルを削除する
$ rm db/development.sqlite3

# マイグレートの実行
$ rails db:migrate

このようにデータベースのカラム更新はエラーが発生しやすく注意が必要なステップとなります。
開発環境であれば気軽にリセットできますが本番環境ではデリケートな問題となりますので、最初にしっかりとした設計をしておくことが重要です。
データベース設計の考え方を身につけるにはある程度の経験も必要になりますから、普段から意識していきましょう!

一対一のアソシエーション

Railsではモデル同士の関連付けのことをアソシエーションといいます。

最初に示した仕様のNo.1を見ると、「ひとつのイベント(Event)に対してひとつのチケット(Ticket)が対応する」とあります。
このような関係を一対一のリレーション(アソシエーション)といいます。

一対一のアソシエーションをActive Recordで表現するには主キー側のモデルにhas_one、外部キー側のモデルにbelongs_toというクラスメソッドを指定します。
この場合はEventに対応したTicketを用意することになるので、Event側が主キーでTicket側が外部キーです。

class Event < ApplicationRecord
  has_one :ticket
end
class Ticket < ApplicationRecord
  belongs_to :event
end

一対多のアソシエーション

また仕様のNo.2によると、「ひとつの会場(Venue)は複数のイベント(Event)を管理できる」とあります。
こちらは一対多のリレーションといいます。
一対一とは違い、主キー側の値を外部キー側で重複して利用することが可能となっています。

一対多のアソシエーションをActive Recordで表現するには主キー側にhas_many、外部キー側にbelongs_toというクラスメソッドを指定します。

class Venue < ApplicationRecord
  has_many :events
end
class Event < ApplicationRecord
  has_one :ticket
  belongs_to :venue   # 追加した行
end

アソシエーション先モデルへのアクセス

アソシエーションで結びついたモデルへのレコードの登録、取得を試してみます。
一対一でも一対多でも同様の方法で操作でき、どちらも直感的な記述になっています。

関連付けられたレコードの登録には、以下のようにいくつか記述のパターンがあります。

irb(main):001:0> venue = Venue.new
   (0.8ms)  SELECT sqlite_version(*)
=> #<Venue:0x0000023bf5084340 id: nil, name: nil, capacity: nil, created_at: nil, updated_at: nil>

irb(main):002:0> venue.name = 'Sample Dome'
=> "Sample Dome"

irb(main):003:0> venue.capacity = 2000
=> 2000

irb(main):004:0> venue.save
  TRANSACTION (0.1ms)  begin transaction
  Venue Create (3.1ms)  INSERT INTO "venues" ("name", "capacity", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Sample Dome"], ["capacity", 2000], ["created_at", "2021-11-07 01:27:07.550543"], ["updated_at", "2021-11-07 01:27:07.550543"]]
  TRANSACTION (2.9ms)  commit transaction
=> true
irb(main):005:0> event = venue.events.new
=> #<Event:0x000001f8330b90a0 id: nil, name: nil, hold_date: nil, created_at: nil, updated_at: nil, venue_id: 1>

irb(main):006:0> event.name = 'sample event'
=> "sample event"

irb(main):007:0> event.hold_date = Time.parse('20220101')
=> 2022-01-01 00:00:00 +0900

irb(main):008:0> event.save
  TRANSACTION (0.2ms)  begin transaction
  Event Create (1.1ms)  INSERT INTO "events" ("name", "hold_date", "created_at", "updated_at", "venue_id") VALUES (?, ?, ?, ?, ?)  [["name", "sample event"], ["hold_date", "2022-01-01"], ["created_at", "2021-11-07 01:32:38.310976"], ["updated_at", "2021-11-07 01:32:38.310976"], ["venue_id", 1]]
  TRANSACTION (4.3ms)  commit transaction
=> true
irb(main):009:1* venue.events.create(
irb(main):010:1*   name: 'sample event 2',
irb(main):011:1*   hold_date: Time.parse('20230101'),
irb(main):012:0> )
  TRANSACTION (0.1ms)  begin transaction
  Event Create (2.6ms)  INSERT INTO "events" ("name", "hold_date", "created_at", "updated_at", "venue_id") VALUES (?, ?, ?, ?, ?)  [["name", "sample event 2"], ["hold_date", "2023-01-01"], ["created_at", "2021-11-07 01:34:01.331830"], ["updated_at", "2021-11-07 01:34:01.331830"], ["venue_id", 1]]
  TRANSACTION (2.2ms)  commit transaction
=> #<Event:0x000001f8330b07e8 id: 2, name: "sample event 2", hold_date: Sun, 01 Jan 2023, created_at: Sun, 07 Nov 2021 01:34:01.331830000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:34:01.331830000 UTC +00:00, venue_id: 1>
irb(main):013:1* venue.events << Event.create(
irb(main):014:1*   name: 'sample event 3',
irb(main):015:1*   hold_date: Time.parse('20240101'),
irb(main):016:0> )
  TRANSACTION (0.1ms)  begin transaction
  Event Create (2.6ms)  INSERT INTO "events" ("name", "hold_date", "created_at", "updated_at", "venue_id") VALUES (?, ?, ?, ?, ?)  [["name", "sample event 3"], ["hold_date", "2024-01-01"], ["created_at", "2021-11-07 01:36:59.780439"], ["updated_at", "2021-11-07 01:36:59.780439"], ["venue_id", 1]]
  TRANSACTION (2.8ms)  commit transaction
  Event Load (0.3ms)  SELECT "events".* FROM "events" WHERE "events"."venue_id" = ?  [["venue_id", 1]]
=>
[#<Event:0x000001f8330b90a0 id: 1, name: "sample event", hold_date: Sat, 01 Jan 2022, created_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, venue_id: 1>,
 #<Event:0x000001f8330b07e8 id: 2, name: "sample event 2", hold_date: Sun, 01 Jan 2023, created_at: Sun, 07 Nov 2021 01:34:01.331830000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:34:01.331830000 UTC +00:00, venue_id: 1>,
 #<Event:0x000001f832952a78 id: 3, name: "sample event 3", hold_date: Mon, 01 Jan 2024, created_at: Sun, 07 Nov 2021 01:36:59.780439000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:36:59.780439000 UTC +00:00, venue_id: 1>]

また、関連するレコードにアクセスする方法は以下の通りです。
to_aメソッドを利用することで、レコードを配列として取得することもできます。

irb(main):001:0> venue = Venue.find(1)
   (0.5ms)  SELECT sqlite_version(*)
  Venue Load (0.2ms)  SELECT "venues".* FROM "venues" WHERE "venues"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<Venue:0x00000252ead35088 id: 1, name: "Sample Dome", capacity: 2000, created_at: Sun, 07 Nov 2021 01:27:07.550543000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:27:07.550543000 UTC +00:00>

irb(main):002:0> venue.events.to_a
  Event Load (0.2ms)  SELECT "events".* FROM "events" WHERE "events"."venue_id" = ?  [["venue_id", 1]]
=>
[#<Event:0x00000252ea74bdc0 id: 1, name: "sample event", hold_date: Sat, 01 Jan 2022, created_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, venue_id: 1>,
 #<Event:0x00000252eaa4c808 id: 2, name: "sample event 2", hold_date: Sun, 01 Jan 2023, created_at: Sun, 07 Nov 2021 01:34:01.331830000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:34:01.331830000 UTC +00:00, venue_id: 1>,
 #<Event:0x00000252eaa4c600 id: 3, name: "sample event 3", hold_date: Mon, 01 Jan 2024, created_at: Sun, 07 Nov 2021 01:36:59.780439000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:36:59.780439000 UTC +00:00, venue_id: 1>]

irb(main):003:0> event = Event.find(1)
  Event Load (0.3ms)  SELECT "events".* FROM "events" WHERE "events"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<Event:0x00000252eab06a78 id: 1, name: "sample event", hold_date: Sat, 01 Jan 2022, created_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, venue_id: 1>

irb(main):004:0> event.venue.name
  Venue Load (0.2ms)  SELECT "venues".* FROM "venues" WHERE "venues"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> "Sample Dome"

プログラミングでわからないことがあったら、その道のプロに質問するのが一番!
現役で活躍中のエンジニアと繋がって、効率的にWeb開発のスキルを身につけましょう。
プログラミング学習のスキルプラットフォーム【MENTA】

多対多のアソシエーション

続いて仕様のNo.3にある、

  • ひとつのイベント(Event)に対して複数人のゲスト(Guest)が参加できる
  • また、ゲスト(Guest)は複数のイベント(Event)に参加できる

の部分について考えていきます。
このような関係を多対多のリレーションといいます。
多対多のリレーションを表現するには、中間モデル(中間テーブル)というアソシエーションを示すためだけの別のモデルを用意する必要があります。
中間モデルには、関連付けたい2つのテーブルの外部キーを両方用意します。

中間モデル(中間テーブル)の作成

EventモデルとGuestモデルを関連付けるためにEventGuestというモデルを作成します。

$ rails generate model EventGuest

各モデルに対して外部キーを設定しておきます。

class CreateEventGuests < ActiveRecord::Migration[6.1]
  def change
    create_table :event_guests do |t|
      t.references :event, null:false, foreign_key: true
      t.references :guest, null: false, foreign_key: true

      t.timestamps
    end
  end
end
$ rails db:migrate

アソシエーションの設定

EventモデルとGuestモデルに対して、クラスメソッドのhas_manyを以下のように設定すれば、多対多のリレーションが実現できます。

class Event < ApplicationRecord
  has_many :event_guests
  has_many :guests, through: :event_guests
end
class Guest < ApplicationRecord
  has_many :event_guests
  has_many :events, through: :event_guests
end

中間モデルは以下のようにbelongs_toで各モデルと結びつけます。

class EventGuest < ApplicationRecord
  belongs_to :event
  belongs_to :guest
end

アソシエーション先モデルへのアクセス

サンプルデータを投入してデータの取得を試してみましょう。

$ rails console

irb(main):001:0> Rick = Guest.create( name: "Rick" )
irb(main):002:0> Kevin = Guest.create( name: "Kevin" )

以下のようにして、アソシエーションを用いてデータの登録することができます。

irb(main):003:0> rick.events << Event.find(1)
  Event Load (0.6ms)  SELECT "events".* FROM "events" WHERE "events"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  EventGuest Create (2.3ms)  INSERT INTO "event_guests" ("event_id", "guest_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["event_id", 1], ["guest_id", 1], ["created_at", "2021-11-07 04:33:04.421690"], ["updated_at", "2021-11-07 04:33:04.421690"]]
  TRANSACTION (3.1ms)  commit transaction
  Event Load (0.2ms)  SELECT "events".* FROM "events" INNER JOIN "event_guests" ON "events"."id" = "event_guests"."event_id" WHERE "event_guests"."guest_id" = ?  [["guest_id", 1]]
=>
[#<Event:0x000001b93bb054f0
  id: 1,
  name: "sample event", hold_date: Sat, 01 Jan 2022, created_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, venue_id: 1>]

irb(main):004:0> kevin.events << Event.find(1)
  Event Load (0.2ms)  SELECT "events".* FROM "events" WHERE "events"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  EventGuest Create (35.1ms)  INSERT INTO "event_guests" ("event_id", "guest_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["event_id", 1], ["guest_id", 2], ["created_at", "2021-11-07 04:33:48.382148"], ["updated_at", "2021-11-07 04:33:48.382148"]]
  TRANSACTION (16.4ms)  commit transaction
  Event Load (0.2ms)  SELECT "events".* FROM "events" INNER JOIN "event_guests" ON "events"."id" = "event_guests"."event_id" WHERE "event_guests"."guest_id" = ?  [["guest_id", 2]]
=> [#<Event:0x000001b93bfc7d08 id: 1, name: "sample event", hold_date: Sat, 01 Jan 2022, created_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, venue_id: 1>]

アソシエーション先のレコードへの操作も以下のようにして簡単に記述することができます。

irb(main):005:0> event = Event.find(1)
  Event Load (0.2ms)  SELECT "events".* FROM "events" WHERE "events"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<Event:0x000001b93c4ac918 id: 1, name: "sample event", hold_date: Sat, 01 Jan 2022, created_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:32:38.310976000 UTC +00:00, venue_id: 1>

irb(main):006:0> event.guests.count
   (0.3ms)  SELECT COUNT(*) FROM "guests" INNER JOIN "event_guests" ON "guests"."id" = "event_geusts"."guest_id" WHERE "event_guests"."event_id" = ?  [["event_id", 1]]
=> 2

プログラミングを教えたい、または学びたいなら、エンジニアに質問し放題のスキルプラットフォームを利用しましょう。
非常に安価で始められるので、スクールに抵抗がある方にもおすすめです。
「教えたい人」と「学びたい人」をつなぐ【MENTA】

アソシエーションのオプション

アソシエーションには様々なオプションをつけることができます。
より柔軟な使い方ができるようになるので是非マスターしましょう。

オートセーブ

autosaveオプションでは、一対一のアソシエーションで関連付けられたレコードのデータを保存する際の挙動を変更できます。

autosaveオプションを指定しない場合(デフォルト)は、少しわかりにくい挙動になります。
Eventモデルから対応するTicketモデルを直接作成(ビルド)した場合を見ていきます。

$ rails console

irb(main):001:0> event = Event.new
   (1.4ms)  SELECT sqlite_version(*)
=> #<Event:0x0000014628750bd8 id: nil, name: nil, hold_date: nil, created_at: nil, updated_at: nil, venue_id: nil>

irb(main):002:0> venue = Venue.find(1)
  Venue Load (0.7ms)  SELECT "venues".* FROM "venues" WHERE "venues"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<Venue:0x0000014628636c48 id: 1, name: "Sample Dome", capacity: 2000, created_at: Sun, 07 Nov 2021 01:27:07.550543000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:27:07.550543000 UTC +00:00>

irb(main):003:0> event.build_ticket
=> #<Ticket:0x000001462748d7a8 id: nil, name: nil, created_at: nil, updated_at: nil, event_id: nil>

irb(main):004:0> event.name = 'sample event 4'
=> "sample event 4"

irb(main):005:0> event.hold_date = Time.current.since(30.days)
=> Tue, 07 Dec 2021 05:16:50.930943000 UTC +00:00

irb(main):006:0> event.venue = venue
=> #<Venue:0x0000014628636c48 id: 1, name: "Sample Dome", capacity: 2000, created_at: Sun, 07 Nov 2021 01:27:07.550543000 UTC +00:00, updated_at: Sun, 07 Nov 2021 01:27:07.550543000 UTC +00:00>

irb(main):007:0> event.ticket.fee = 1000
=> 1000

irb(main):008:0> event.save
  TRANSACTION (0.1ms)  begin transaction
  Event Create (1.2ms)  INSERT INTO "events" ("name", "hold_date", "created_at", "updated_at", "venue_id") VALUES (?, ?, ?, ?, ?)  [["name", "sample event 4"], ["hold_date", "2021-12-07"], ["created_at", "2021-11-07 05:17:18.013916"], ["updated_at", "2021-11-07 05:17:18.013916"], ["venue_id", 1]]
  Ticket Create (1.5ms)  INSERT INTO "tickets" ("fee", "created_at", "updated_at", "event_id") VALUES (?, ?, ?, ?)  [["fee", 1000], ["created_at", "2021-11-07 05:17:18.017649"], ["updated_at", "2021-11-07 05:17:18.017649"], ["event_id", 4]]
  TRANSACTION (2.5ms)  commit transaction
=> true

irb(main):009:0> Ticket.last.fee
  Ticket Load (0.2ms)  SELECT "tickets".* FROM "tickets" ORDER BY "tickets"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 1000

しかし、一度作成した後にレコードを更新しようとしても、子モデルのデータベースは更新されません。
この挙動は非常にややこしいですね。

irb(main):010:0> event.ticket.fee = 2000
=> 2000

irb(main):011:0> event.save
=> true

irb(main):012:0> Ticket.last.fee
=> 1000

一方、Ticket(子モデル側)のbelongs_toにautosaveオプションを付けた場合の挙動は以下の通りです。
autosaveを指定しない場合とautosaveをfalseにした場合は意味が異なるので注意が必要です。

autosaveをtrueにした場合は、CreateとUpdateの両方で子モデルがデータベースに保存されます。
autosaveをfalseにした場合は、子モデルはデータベースに保存されません。

class Ticket < ApplicationRecord
  belongs_to :event, autosave: true
end

データベースへの保存についてそれぞれまとめると以下のようになります。

ActiveRecordの設定CREATEUPDATE
デフォルト×
autosaveオプション true
autosaveオプション false××

自動削除

dependentオプションでは、親モデルが削除されたときにそれに関連する子モデルをどう処理するかを指定します。
基本的に親モデルを失った子モデルは意味を持たなくなる場合が多いので、よく使うオプションになります。

destroyを指定すると、親モデルのレコードが削除されたときに関連する子モデルのレコードも削除されます。
delete、またはdelete_allを指定すると、親モデルのレコードが削除されたときに関連する子モデルのレコードが直接データベースから削除されます(has_oneの場合にはdelete、has_manyの場合にはdelete_allを使います)。
nullifyを指定すると、親モデルのレコードが削除されたときに関連する子モデルの外部キーがnilに更新されます。

destroyとdelete(delete_all)は似ていますが、基本的にはdestroyを指定します。
ただ、destroyは関連する子モデルの数だけdestoryを実行することになり、速度が遅い・サーバへの負荷が大きいといったデメリットがあることには注意が必要です。
そのため、一対多のリレーションで親モデルのレコードに関連する子モデルのレコードが多い場合にはdelete(delete_all)が有効と言えます。

また、レコードを非破壊で無効化したい場合にはnullifyを指定します。

class Venue < ApplicationRecord
  has_many :events, dependent: destroy
end

更新日時の自動更新

touchオプションを指定すると、モデルを更新した際に関連付けられたモデルの最終更新日時(updated_on)タイムスタンプを現在時刻に更新します。

class Venue < ApplicationRecord
  has_many :events, touch: true
end

まとめ

今回はActiveRecordのアソシエーションを利用して、あるモデルから関連するモデルへのアクセスを行いました。
アソシエーションには一対一、一対多、多対多の3種類があります。

一対一、一対多は外部キー制約を設定し、モデルにそれぞれhas_oneやhas_many、belongs_toといったクラスメソッドを指定するだけで実現することができました。

多対多のアソシエーションを設定したい場合は、中間テーブルを用いて双方からhas_manyを指定します。

ひとりではなかなかプログラミングの学習が続かない、未経験だから不安が多い、という方はプログラミングスクールを利用してみるのも有効です。
本物のエンジニアに学ぶことで、時間がない方でも最短でスキルを身につけることができます。
現役エンジニアのパーソナルメンターからマンツーマンで学べる

お悩みの方は、まずは無料キャリアカウンセリングにお申込みください。

関連記事

この記事のタグ