Railsで「複数カラム」かつ「共通テーブル」にアソシエーション(1対多・多対多・自己結合)

# はじめに
最近`Ruby on Rails`を勉強していて，アソシエーションについての知見が増えたので，せっかくなので共有したいと思います．
テーブルの複数カラムから共通のテーブルを参照するときや，別名主キーを設定したテーブルに対してアソシエーションを張る方法について紹介していきます．

今回紹介するアソシエーションをすべてまとめると以下の図のようになります．
![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/ba1dc171-7269-f7fe-40a0-7663eb5d0012.png)

同じものを作る場合上から順に実装を進めていけばうまくいくと思います．

<font color="red">
※めちゃ長いです。すみません。
</font>

# もくじ
- [複数カラム・共通テーブル・1対多](#複数カラム共通テーブル1対多)
    - [テーブルの作成](#テーブルの作成)
    - [アソシエーションを追加](#アソシエーションを追加)
- [共通テーブル・多対多](#共通テーブル多対多)
    - [テーブルの作成](#テーブルの作成-1)
    - [アソシエーションを追加](#アソシエーションを追加-1)
- [自己結合・多対多](#自己結合多対多)
    - [テーブルの作成](#テーブルの作成-2)
    - [アソシエーションを追加](#アソシエーションを追加-2)
- [複数カラム・共通テーブル・1対多＆多対多](#複数カラム共通テーブル1対多多対多)
    - [テーブルの作成](#テーブルの作成-3)
    - [アソシエーションを追加](#アソシエーションを追加-3)
- [さいごに](#さいごに)
- [参考記事](#参考記事)

# 複数カラム・共通テーブル・1対多
以下の図のようなアソシエーションを想定します．
![1_many2one_same.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/e527dac1-5043-672a-f401-290f1541be56.png)
`Item(製品)`は1社の`Vendor(販売業者)`と1社の`Manufacturer(製造業者)`を持ち，`Company(会社)`は複数の`Item(製品)`を持つ可能性があります．

複数のカラムから共通のテーブルに対して，1対多の関係を持たせるため，別名での参照が必要になってきます．

また，`Company(会社)`は主キーとして文字列型の`company_code`を持ちます．

## テーブルの作成
### `Companyテーブル`
```bash
$ rails g model Company
```
マイグレーションファイルを修正します．

```ruby:db/migrate/20211210133333_create_companies.rb
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`で新たな主キーを設定します．


マイグレーションを行います．

```bash
$ rails db:migrate
```

### `Itemテーブル`
```bash
$ rails g model Item
```
こちらには`t.references`で外部キー制約を追加します．

```ruby:db/migrate/20211210133508_create_items.rb
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`であると明示してあげます．

```ruby
    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`という存在しない主キーを参照しようとしてエラーを起こしてしまいます．

設定したら，マイグレーションをして終了です．

```bash
$ rails db:migrate
```

## アソシエーションを追加
外部キーを設定したので，それらを参照できるようにモデルにアソシエーションを追加します．


```ruby:app/models/item.rb
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へ保存できなくなります．
今回は，後から外部キー制約を追加するのですが，こういう時は必要となる設定です．

```ruby:app/models/company.rb
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`を設定します．複数保有する可能性があるため複数形で命名しています．

# 共通テーブル・多対多
以下の図のようなアソシエーションを想定します．
![4_2_many2one_many2many_same.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/43e2dae1-b619-358f-59d8-2090f71e7941.png)
`User(ユーザ)`は複数の`Item(製品)`を持ち，`User(ユーザ)`は複数の欲しい`Item(製品)`があります．`Item(製品)`は複数のユーザから関連付けられます．
以上から，`共通のテーブル(Item)`に対して`User`から二通りの参照が行われる多対多の関係であることが分かります．

Ruby on Railsでは，多対多を実現する場合，`中間テーブル`を用いて実装を行います．
今回は，`UserWishlist(ユーザの欲しいものリスト用)`テーブルと`UserHaving(ユーザの所有物リスト用)`テーブルを用意します．

## テーブルの作成
### `Userテーブル`
`name`だけを持つ`User`テーブルを作成します．

```bash
$ rails g model user name
$ rails db:migrate
```

### `中間テーブル`
`UserWishlist`テーブルと`UserHaving`テーブルを作成します．

```bash=
$ rails g model UserHaving
$ rails g model UserWishlist
```
マイグレーションファイルを編集します．

```ruby:db/migrate/20211210141358_create_user_havings.rb
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`テーブルを参照していることを明示します．

```ruby:db/migrate/20211210141411_create_user_wishlists.rb
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`テーブルを参照していることを明示します．

設定を完了したら，マイグレーションをして終了です．

```bash
$ rails db:migrate
```

## アソシエーションを追加
それぞれにアソシエーションを追加していきます．

```ruby:app/models/user_having.rb
class UserHaving < ApplicationRecord
  belongs_to :having, class_name: 'Item'
  belongs_to :user
end
```
`class_name: 'Item'`を書くことで，`having`が`Item`を参照していることを示します．

```ruby:app/models/user_wishlist.rb
class UserWishlist < ApplicationRecord
  belongs_to :wishlist, class_name: 'Item'
  belongs_to :user
end
```
`class_name: 'Item'`を書くことで，`wishlist`が`Item`を参照していることを示します．

```ruby:app/models/user.rb
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)`にアクセスすることを示しています．

```ruby:app/models/item.rb
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`を示しているのかわからないという風なエラーが出てきます．

# 自己結合・多対多
以下の図のようなアソシエーションを想定します．
![3_own.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/588380bd-68a9-e11b-84fb-92c9b8abfff1.png)
`User(自分)`は複数の`User(友人)`を持ち，その逆も然りという関係です．
自己結合の多対多関係も，中間テーブルを用いて実装します．

## テーブルの作成
### `Friendshipテーブル`
交友関係を管理する中間テーブルとして，`Friendship`テーブルを作成します．

```bash
$ rails g model friendship
```
マイグレーションファイルを修正します．

```ruby:db/migrate/20211210143456_create_friendships.rb
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をユニークで張ることで，組み合わせを固有にし，友達なのに何度も友達になるようなこと回避します．

マイグレーションをして終了です．

```bash
$ rails db:migrate
```
## アソシエーションを追加
```ruby:app/models/friendship.rb
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`であることを許可しないようにします．

```ruby:app/models/user.rb
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
```
実装は以下の記事を参考(ほぼコピー:sweat:)にさせていただきました．
めちゃくちゃ詳しく自己結合について書いてくださっているため，ここでの説明は省かせていただきます．

参考：[Railsでフォロー機能を作る方法](https://qiita.com/mitsumitsu1128/items/e41e2ff37f143db81897)


# 複数カラム・共通テーブル・1対多＆多対多
以下の図のようなアソシエーションを想定します．
![4_2_many2one_many2many_same.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506931/c0ce2a37-90a7-53a1-c567-ef4f5eeddb36.png)

`Company(会社)`は一つの`Prefecture(本社の都道府県)`を持ちます．
`Company(会社)`は複数の`Prefecture(支社の都道府県)`を持ちます．
`Prefecture(都道府県)`には，複数の`Company(会社)`が存在する可能性があります．

共通のテーブルを`多対多`と`1対多`で参照するようなシチュエーションです．


## テーブルの作成
```bash
$ rails g model prefecture name
$ rails db:migrate
$ rails g model company_branch
$ rails g migration AddReferenceToCompany
```
マイグレーションファイルを修正します．

```ruby:db/migrate/20211211145652_create_company_branches.rb
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
```
今までの流れを追っていれば特に説明する必要もないかと思います．

```ruby:db/migrate/20211214033902_add_reference_to_company.rb
class AddReferenceToCompany < ActiveRecord::Migration[6.1]
  def change
    add_reference :companies, :prefecture, foreign_key: true
  end
end
```
`Company`に外部キーとして`Prefecture`を追加します．

マイグレーションして終了です．

```bash
$ rails db:migrate
```

## アソシエーションを追加
### 多対多を追加
```ruby:app/models/company_branch.rb
class CompanyBranch < ApplicationRecord
  belongs_to :branch, class_name: "Prefecture", foreign_key: :prefecture_id
  belongs_to :company
end
```
`class_name: "Prefecture"`で`branch`が`Prefecture`を参照することを示し，その外部キーは`prefecture_id`であることを明記しています．

```ruby:app/models/prefecture.rb
class Prefecture < ApplicationRecord
  has_many :company_branches, dependent: :destroy
  has_many :branches, through: :company_branches
end
```
こちらは，先ほどもやったような別名の多対多アソシエーションです．

```ruby:app/models/company.rb
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対多を追加
```ruby:app/models/company.rb
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`を参照していることを示します．

```ruby:app/models/prefecture.rb
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`を参照することを示します．

# データを登録してみる
```ruby:seed.rb
cmp1 = Company.create(name: "販売店１", company_code: "v-001")
cmp2 = Company.create(name: "販売店２", company_code: "v-002")
cmp3 = Company.create(name: "販売店３", company_code: "v-003")
cmp4 = Company.create(name: "製造業者１", company_code: "m-001")
cmp5 = Company.create(name: "製造業者２", company_code: "m-002")
cmp6 = Company.create(name: "製造業者３", company_code: "m-003")
itm = Item.create(name: "製品１")
itm.vendor = cmp1
itm.manufacturer = cmp4
itm.save
itm2 = Item.create(name: "製品２")
itm2.vendor = cmp2
itm2.manufacturer = cmp5
itm2.save
itm3 = Item.create(name: "製品３")
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]
```
例えば以下のようなことができます．

```ruby
> User.first.friends.first.wishlists.first.vendor.prefecture
```
これは，「ユーザ(1)」の「友達(1)」の「欲しいもの(1)」の「販売会社」の「本社」を取得ということです．
Railsコンソールで試したレスポンスは以下になりました．

```ruby
#<Prefecture:0x000055cc4a07e0b0
 id: 2,
 name: "東京都">
```
# さいごに
いかがでしょうか？

共通のテーブルを参照する外部キーカラムのアソシエーションをいろいろ扱ってみました．

共通テーブルを参照する際は，命名が競合してしまわないように細心の注意を払わなければいけないなと，記事用にプログラムを組んでみて再認識しました．
みなさまもお気を付けください．

それと，参考にさせていただいた様々な記事を最後に載せているので，さらに詳しく実装を知りたい方は，ぜひご覧になってください！

ソースコードもおいておきますね！

https://github.com/yosse95ai/rails_qiita_demo

それでは:wave:

# 参考記事
- **全体**
    - [Railsで主キーをid以外に設定し、ほかのテーブルとアソシエーションを結ぶ](https://qiita.com/tanutanu/items/6c53b5290eaa9bf905d3)
    - [Ruby on Railsで多対多のテーブル設計を動かしながら理解してみる](https://qiita.com/ryutaro9595/items/e021eb789914cead4677)
    - [同じテーブル内に複数の外部キーを設定する方法！！](https://qiita.com/takenobou/items/27ddaef439e2777d875c)
- **複数カラム・共通テーブル・1対多**
    - [Rails でテーブル内の複数カラムから共通のテーブルを別名でリレーションする方法が分からない](https://ja.stackoverflow.com/questions/83944/rails-%e3%81%a7%e3%83%86%e3%83%bc%e3%83%96%e3%83%ab%e5%86%85%e3%81%ae%e8%a4%87%e6%95%b0%e3%82%ab%e3%83%a9%e3%83%a0%e3%81%8b%e3%82%89%e5%85%b1%e9%80%9a%e3%81%ae%e3%83%86%e3%83%bc%e3%83%96%e3%83%ab%e3%82%92%e5%88%a5%e5%90%8d%e3%81%a7%e3%83%aa%e3%83%ac%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3%e3%81%99%e3%82%8b%e6%96%b9%e6%b3%95%e3%81%8c%e5%88%86%e3%81%8b%e3%82%89%e3%81%aa%e3%81%84)
- **共通テーブル・多対多**
    - [Rails で複数の中間テーブルの作成方法がわからない](https://ja.stackoverflow.com/questions/82737/rails-%e3%81%a7%e8%a4%87%e6%95%b0%e3%81%ae%e4%b8%ad%e9%96%93%e3%83%86%e3%83%bc%e3%83%96%e3%83%ab%e3%81%ae%e4%bd%9c%e6%88%90%e6%96%b9%e6%b3%95%e3%81%8c%e3%82%8f%e3%81%8b%e3%82%89%e3%81%aa%e3%81%84)
    - [Rails で多対多のコントローラの作り方が分からない](https://ja.stackoverflow.com/questions/82763/rails-%e3%81%a7%e5%a4%9a%e5%af%be%e5%a4%9a%e3%81%ae%e3%82%b3%e3%83%b3%e3%83%88%e3%83%ad%e3%83%bc%e3%83%a9%e3%81%ae%e4%bd%9c%e3%82%8a%e6%96%b9%e3%81%8c%e5%88%86%e3%81%8b%e3%82%89%e3%81%aa%e3%81%84)
- **自己結合・多対多**
    - [Railsでフォロー機能を作る方法](https://qiita.com/mitsumitsu1128/items/e41e2ff37f143db81897)