CodeBox CodeBox

【Rails】中間テーブルで多対多のアソシエーションを作る

Ruby / Rails
けい

概要

多対多のアソシエーションを作る際、中間テーブルが必要になります。今回は①モデルの定義 ②中間テーブルの作成 ③データの引き出し方についてまとめます。

なぜ中間テーブルが必要?

例えばCustomer has many teams かつ Team has many customersのような多対多の状況を考えてみます。中間テーブルを使わなければ、下画像のようになります。

1つのカラムに複数のデータを入れることができないため、Team数が増えるためその数だけカラムが増えていきます。これは現実的ではありませんね。
中間テーブルを使わないパターン

なのでcustomer_idとteam_idのみ保存する中間テーブルを作成すると、無駄なカラムも増えません。
中間テーブルを使うパターン

準備編

今回は記載しませんが、ScaffoldのAPIモードでアプリの雛形を作成しています。

1.モデルの作成

Customer / Team / CustomerTeamを作成します。

[Customerモデルの作成]
rails g model Customer name:string age:integer

[Teamモデルの作成]
rails g model Team name:string

[CustomerTeamモデルの作成]
rails g model CustomerTeam customer:references team:references


2.マイグレーションファイルを確認

1の作業で下記のようなマイグレーションが作成されます。

[CustomerTable]
class CreateCustomers < ActiveRecord::Migration[6.0]
  def change
    create_table :customers do |t|
      t.string :name
      t.integer :age
      t.timestamps
    end
  end
end


[TeamTable]
class CreateTeams < ActiveRecord::Migration[6.0]
  def change
    create_table :teams do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end


[CustomerTeamTable]
class CreateCustomerTeams < ActiveRecord::Migration[6.0]
  def change
    create_table :customer_teams do |t|
      t.references :customer, foreign_key: true
      t.references :team, foreign_key: true
      t.timestamps
    end
  end
end


migrationを実行してテーブルを作成します。

rails db:migrate


3.モデルの更新

アソシエーションを構築するため、各モデルを更新します。

[customer.rb]
class Customer < ApplicationRecord
    accepts_nested_attributes_for :orders, allow_destroy: true
    has_many :customer_teams
    has_many :teams, through: :customer_teams
end


teamのデータを作成するのと同時に、customer_teamテーブルへデータを作成するように『accepts_nested_attributes_for』を使用しています。使い方は別の記事にまとめているので、参考にくださいね。

[team.rb]
class Team < ApplicationRecord
    has_many :customer_teams
    has_many :customers, through: :customer_teams
    accepts_nested_attributes_for :customer_teams, allow_destroy: true
end


CustomerTeamは1つのcustomerと1つのteamに属しているので、belongs_toを使います。

[customer_team.rb]
class CustomerTeam < ApplicationRecord
    belongs_to :customer
    belongs_to :team
end


4.seedファイルを作成&実行

seedファイルを作成します。

10.times do |n|
    name = (0...8).map{ (65 + rand(26)).chr }.join
    age = Random.new().rand(15..65)
    id = n + 1
    customer = Customer.new

    if customer.new_record?
      customer.name = name
      customer.age = age
      customer.save!
    end

    team_name = (0...5).map{ (65 + rand(26)).chr }.join
    team = Team.new
    if team.new_record?
      team.name = team_name
      team.save!
    end
end

Customer.all.each do | customer |
  id = customer.id
  Team.all.each do | team |
    team_id = team.id
    CustomerTeam.create({customer_id: id, team_id: team_id})
  end
end


データを作成します。

rails db:seed


確認編

中間テーブルを作って、多対多の関係性ができているか確認します。先程Seedファイルで作成したデータをコンソールで使ってみましょう。

irb(main):000:0> customer = Customer.first
#実行結果
#<Customer:0x00007f63c7c3a0f0
 id: 1,
 name: "KPMJKVMV",
 age: 61,
 created_at: Tue, 28 Dec 2021 04:52:18 UTC +00:00,
 updated_at: Tue, 28 Dec 2021 04:52:18 UTC +00:00>


irb(main):001:0> customer.customer_teams
#実行結果
[#<CustomerTeam:0x00007f63c5062798
  id: 1,
  customer_id: 1,
  team_id: 1,
  created_at: Tue, 28 Dec 2021 04:52:18 UTC +00:00,
  updated_at: Tue, 28 Dec 2021 04:52:18 UTC +00:00>,
 #<CustomerTeam:0x00007f63c508b008
  id: 2,
  customer_id: 1,
  team_id: 2,
  created_at: Tue, 28 Dec 2021 04:52:18 UTC +00:00,
  updated_at: Tue, 28 Dec 2021 04:52:18 UTC +00:00>,
.....(省略)


irb(main):002:0> team = customer.customer_teams.where(team_id: 1)
irb(main):003:0> team.name
#実行結果
=> "CustomerTeam"


中間テーブル経由でteamの情報も取得できることが分かりました。
解説は以上です。

ABOUT ME

けい
ベンチャーのフロントエンジニア。 主にVueとTypescriptを使っています。ライターのための文字数カウントアプリ:https://easy-count.vercel.app/