はじめに
最近Ruby on Rails
を勉強していて,アソシエーションについての知見が増えたので,せっかくなので共有したいと思います.
テーブルの複数カラムから共通のテーブルを参照するときや,別名主キーを設定したテーブルに対してアソシエーションを張る方法について紹介していきます.
今回紹介するアソシエーションをすべてまとめると以下の図のようになります.
同じものを作る場合上から順に実装を進めていけばうまくいくと思います.
※めちゃ長いです。すみません。もくじ
複数カラム・共通テーブル・1対多
以下の図のようなアソシエーションを想定します.
Item(製品)
は1社のVendor(販売業者)
と1社のManufacturer(製造業者)
を持ち,Company(会社)
は複数のItem(製品)
を持つ可能性があります.
複数のカラムから共通のテーブルに対して,1対多の関係を持たせるため,別名での参照が必要になってきます.
また,Company(会社)
は主キーとして文字列型のcompany_code
を持ちます.
テーブルの作成
Companyテーブル
$ rails g model Company
マイグレーションファイルを修正します.
class CreateCompanies < ActiveRecord::Migration[6.1]
def change
create_table :companies, id: false do |t|
t.string :company_code, null: false, primary_key: true
t.string :name
t.string :address
t.string :mail
t.string :phone
end
end
end
create_table :companies, id: false do |t|
で自動生成の主キーを無効にします.
そして,t.string :company_code, null: false, primary_key: true
で新たな主キーを設定します.
マイグレーションを行います.
$ rails db:migrate
Itemテーブル
$ rails g model Item
こちらにはt.references
で外部キー制約を追加します.
class CreateItems < ActiveRecord::Migration[6.1]
def change
create_table :items do |t|
t.string :name, null: false
t.references :vendor, type: :string
t.references :manufacturer, type: :string
end
add_foreign_key :items, :companies, column: :vendor_id, primary_key: :company_code
add_foreign_key :items, :companies, column: :manufacturer_id, primary_key: :company_code
end
end
しかし,参照先は自動生成の主キーではないため,よく見る記事の外部キー制約とは違います.
t.references
にはforeign_key: true
を設定せず,キーの型がstring
であるということだけ追加します.
また,外部キー名はcompany
ではなく,別名としてvendor
とmanufacturer
とつけます.
上で外部キーであることを明示していない代わりに,add_foreign_key
を用いて外部キーの主キーがcompany_code
であると明示してあげます.
add_foreign_key :items, :companies, column: :vendor_id, primary_key: :company_code
add_foreign_key :items, :companies, column: :manufacturer_id, primary_key: :company_code
独自に主キーを設定した場合はこの方法で設定しないと,company_id
という存在しない主キーを参照しようとしてエラーを起こしてしまいます.
設定したら,マイグレーションをして終了です.
$ rails db:migrate
アソシエーションを追加
外部キーを設定したので,それらを参照できるようにモデルにアソシエーションを追加します.
class Item < ApplicationRecord
belongs_to :vendor, class_name: "Company", optional: true
belongs_to :manufacturer, class_name: "Company", optional: true
end
Item
テーブルから,vender
とmanfacturer
でCompany
テーブルを参照できるようにbelongs_to
を設定します.
optional: true
で外部キーがnil
であることを許可します.
belongs_to
では,これがないと,バリデーションにはじかれてDBへ保存できなくなります.
今回は,後から外部キー制約を追加するのですが,こういう時は必要となる設定です.
class Company < ApplicationRecord
self.primary_key = :company_code
has_many :vendees, class_name: "Item", foreign_key: "vendor_id"
has_many :manufacturees, class_name: "Item", foreign_key: "manufacturer_id"
end
self.primary_key = :company_code
でCompany
の主キーがcomapny_code
であることを明示します.
これによって,Company.find("独自の主キー")
での検索ができるようになります.
vendees
とmanfacturees
でItem
テーブルを参照できるようにhas_many
を設定します.複数保有する可能性があるため複数形で命名しています.
共通テーブル・多対多
以下の図のようなアソシエーションを想定します.
User(ユーザ)
は複数のItem(製品)
を持ち,User(ユーザ)
は複数の欲しいItem(製品)
があります.Item(製品)
は複数のユーザから関連付けられます.
以上から,共通のテーブル(Item)
に対してUser
から二通りの参照が行われる多対多の関係であることが分かります.
Ruby on Railsでは,多対多を実現する場合,中間テーブル
を用いて実装を行います.
今回は,UserWishlist(ユーザの欲しいものリスト用)
テーブルとUserHaving(ユーザの所有物リスト用)
テーブルを用意します.
テーブルの作成
Userテーブル
name
だけを持つUser
テーブルを作成します.
$ rails g model user name
$ rails db:migrate
中間テーブル
UserWishlist
テーブルとUserHaving
テーブルを作成します.
$ rails g model UserHaving
$ rails g model UserWishlist
マイグレーションファイルを編集します.
class CreateUserHavings < ActiveRecord::Migration[6.1]
def change
create_table :user_havings do |t|
t.references :having, foreign_key: { to_table: :items }
t.references :user, foreign_key: true
end
end
end
User
とHaving(Item)
を中間テーブルの外部キーとして設定します.
Having
というテーブルは存在しないので,foreign_key: { to_table: :items }
でItem
テーブルを参照していることを明示します.
class CreateUserWishlists < ActiveRecord::Migration[6.1]
def change
create_table :user_wishlists do |t|
t.references :wishlist, foreign_key: { to_table: :items }
t.references :user, foreign_key: true
end
end
end
User
とWishlist(Item)
を中間テーブルの外部キーとして設定します.
Wishlist
というテーブルは存在しないので,foreign_key: { to_table: :items }
でItem
テーブルを参照していることを明示します.
設定を完了したら,マイグレーションをして終了です.
$ rails db:migrate
アソシエーションを追加
それぞれにアソシエーションを追加していきます.
class UserHaving < ApplicationRecord
belongs_to :having, class_name: 'Item'
belongs_to :user
end
class_name: 'Item'
を書くことで,having
がItem
を参照していることを示します.
class UserWishlist < ApplicationRecord
belongs_to :wishlist, class_name: 'Item'
belongs_to :user
end
class_name: 'Item'
を書くことで,wishlist
がItem
を参照していることを示します.
class User < ApplicationRecord
has_many :user_havings, dependent: :destroy
has_many :havings, through: :user_havings
has_many :user_wishlists, dependent: :destroy
has_many :wishlists, through: :user_wishlists
end
through: :user_havings
でUserHaving
テーブルを通して,havings(Item)
にアクセスすることを示しています.
同様に,through: :user_wishlists
でUserWishlist
テーブルを通して,wishlists(Item)
にアクセスすることを示しています.
class Item < ApplicationRecord
belongs_to :vendor, class_name: "Company", optional: true
belongs_to :manufacturer, class_name: "Company", optional: true
# 以下を追加
has_many :user_havings, dependent: :destroy, foreign_key: "having_id"
has_many :had_users, through: :user_havings, source: :user
has_many :user_wishlists, dependent: :destroy, foreign_key: "wishlist_id"
has_many :wished_users, through: :user_wishlists, source: :user
end
ほとんど,user.rb
と同様の考え方なのですが,2点だけ違います.
それは,外部キーとして,having_id
とwishlist_id
を使用するように宣言していることと,source: :user
でUser
を参照するように指定しているところです.
source
を指定しないと,wished_user
が中間テーブルのwishlist
を示しているのか,user
を示しているのかわからないという風なエラーが出てきます.
自己結合・多対多
以下の図のようなアソシエーションを想定します.
User(自分)
は複数のUser(友人)
を持ち,その逆も然りという関係です.
自己結合の多対多関係も,中間テーブルを用いて実装します.
テーブルの作成
Friendshipテーブル
交友関係を管理する中間テーブルとして,Friendship
テーブルを作成します.
$ rails g model friendship
マイグレーションファイルを修正します.
class CreateFriendships < ActiveRecord::Migration[6.1]
def change
create_table :friendships do |t|
t.references :user, foreign_key: true
t.references :friend, foreign_key: { to_table: :users }
t.index %i[user_id friend_id], unique: true
end
end
end
外部キーはuser
とfriend(User)
となります.
Friend
というテーブルはないので,foreign_key: { to_table: :users }
でUser
テーブルを参照していることを明示します.
また,indexをユニークで張ることで,組み合わせを固有にし,友達なのに何度も友達になるようなこと回避します.
マイグレーションをして終了です.
$ rails db:migrate
アソシエーションを追加
class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend, class_name: 'User'
validates :user_id, presence: true
validates :friend_id, presence: true
end
class_name: 'User'
を書くことで,friend
がUser
を参照していることを示します.
また,validates
でどちらか一方でもnull
であることを許可しないようにします.
class User < ApplicationRecord
has_many :user_havings, dependent: :destroy
has_many :havings, through: :user_havings
has_many :user_wishlists, dependent: :destroy
has_many :wishlists, through: :user_wishlists
# 以下を追加
has_many :friendships, dependent: :destroy
has_many :friends, through: :friendships, source: :friend
has_many :my_friendships, class_name: "Friendship", foreign_key: "friend_id", dependent: :destroy
has_many :users, through: :my_friendships, source: :user
def friending(friend)
unless self == friend
self.friendships.find_or_create_by(friend_id: friend.id)
end
end
def unfriending(friend)
friendship_to = self.friendships.find_by(friend_id: friend.id)
friendship_to.destroy if friendship_to
end
end
実装は以下の記事を参考(ほぼコピー)にさせていただきました.
めちゃくちゃ詳しく自己結合について書いてくださっているため,ここでの説明は省かせていただきます.
複数カラム・共通テーブル・1対多&多対多
Company(会社)
は一つのPrefecture(本社の都道府県)
を持ちます.
Company(会社)
は複数のPrefecture(支社の都道府県)
を持ちます.
Prefecture(都道府県)
には,複数のCompany(会社)
が存在する可能性があります.
共通のテーブルを多対多
と1対多
で参照するようなシチュエーションです.
テーブルの作成
$ rails g model prefecture name
$ rails db:migrate
$ rails g model company_branch
$ rails g migration AddReferenceToCompany
マイグレーションファイルを修正します.
class CreateCompanyBranches < ActiveRecord::Migration[6.1]
def change
create_table :company_branches do |t|
t.references :prefecture, foreign_key: true
t.references :company, type: :string
end
add_foreign_key :company_branches, :companies, column: :company_id, primary_key: :company_code
end
end
今までの流れを追っていれば特に説明する必要もないかと思います.
class AddReferenceToCompany < ActiveRecord::Migration[6.1]
def change
add_reference :companies, :prefecture, foreign_key: true
end
end
Company
に外部キーとしてPrefecture
を追加します.
マイグレーションして終了です.
$ rails db:migrate
アソシエーションを追加
多対多を追加
class CompanyBranch < ApplicationRecord
belongs_to :branch, class_name: "Prefecture", foreign_key: :prefecture_id
belongs_to :company
end
class_name: "Prefecture"
でbranch
がPrefecture
を参照することを示し,その外部キーはprefecture_id
であることを明記しています.
class Prefecture < ApplicationRecord
has_many :company_branches, dependent: :destroy
has_many :branches, through: :company_branches
end
こちらは,先ほどもやったような別名の多対多アソシエーションです.
class Company < ApplicationRecord
self.primary_key = :company_code
has_many :vendees, class_name: "Item", foreign_key: "vendor_id"
has_many :manufacturees, class_name: "Item", foreign_key: "manufacturer_id"
# 以下を追加
has_many :company_branches, dependent: :destroy
has_many :branches, through: :company_branches
end
こちらも,先ほどもやったような別名の多対多アソシエーションです.
1対多を追加
class Company < ApplicationRecord
self.primary_key = :company_code
has_many :vendees, class_name: "Item", foreign_key: "vendor_id"
has_many :manufacturees, class_name: "Item", foreign_key: "manufacturer_id"
has_many :company_branches, dependent: :destroy
has_many :branches, through: :company_branches
# 以下を追加
belongs_to :prefecture, class_name: "Prefecture", optional: true
end
class_name: "Prefecture"
でprefecture
はPrefecture
を参照していることを示します.
class Prefecture < ApplicationRecord
has_many :company_branches, dependent: :destroy
has_many :branches, through: :company_branches
# 以下を追加
has_many :headees, class_name: "Company"
end
そして,class_name: "Company"
でheadees
はCompany
を参照することを示します.
データを登録してみる
cmp1 = Company.create(name: "販売店1", company_code: "v-001")
cmp2 = Company.create(name: "販売店2", company_code: "v-002")
cmp3 = Company.create(name: "販売店3", company_code: "v-003")
cmp4 = Company.create(name: "製造業者1", company_code: "m-001")
cmp5 = Company.create(name: "製造業者2", company_code: "m-002")
cmp6 = Company.create(name: "製造業者3", company_code: "m-003")
itm = Item.create(name: "製品1")
itm.vendor = cmp1
itm.manufacturer = cmp4
itm.save
itm2 = Item.create(name: "製品2")
itm2.vendor = cmp2
itm2.manufacturer = cmp5
itm2.save
itm3 = Item.create(name: "製品3")
itm3.vendor = cmp3
itm3.manufacturer = cmp6
itm3.save
f = Prefecture.create(name: "福岡県")
t = Prefecture.create(name: "東京都")
k = Prefecture.create(name: "京都府")
o = Prefecture.create(name: "大阪府")
h = Prefecture.create(name: "北海道")
cmp1.branches = [f, k]
cmp2.branches = [k]
cmp3.branches = [f, k, o, h]
cmp4.branches = [k, h]
cmp5.branches = [f, k, o]
cmp6.branches = [t]
cmp1.prefecture = t
cmp2.prefecture = t
cmp3.prefecture = t
cmp4.prefecture = t
cmp5.prefecture = t
cmp6.prefecture = h
cmp1.save
cmp2.save
cmp3.save
cmp4.save
cmp5.save
cmp6.save
us = User.create(name: "usr01")
us2 = User.create(name: "usr02")
us.friending(us2)
us2.wishlists = [itm3]
例えば以下のようなことができます.
> User.first.friends.first.wishlists.first.vendor.prefecture
これは,「ユーザ(1)」の「友達(1)」の「欲しいもの(1)」の「販売会社」の「本社」を取得ということです.
Railsコンソールで試したレスポンスは以下になりました.
#<Prefecture:0x000055cc4a07e0b0
id: 2,
name: "東京都">
さいごに
いかがでしょうか?
共通のテーブルを参照する外部キーカラムのアソシエーションをいろいろ扱ってみました.
共通テーブルを参照する際は,命名が競合してしまわないように細心の注意を払わなければいけないなと,記事用にプログラムを組んでみて再認識しました.
みなさまもお気を付けください.
それと,参考にさせていただいた様々な記事を最後に載せているので,さらに詳しく実装を知りたい方は,ぜひご覧になってください!
ソースコードもおいておきますね!
それでは