【Rails】たくさんあるprimary_keyとforeign_keyの設定について、それぞれの役割を理解する

## はじめに
Railsで主キーや外部キーを指定する際、`primary_key`や`foreign_key`をモデルに設定します。
```モデル例.rb
class Office < ApplicationRecord
  self.primary_key = :company_cd
  has_many :users, primary_key: :office_cd, foreign_key: :company_cd
end

class User < ApplicationRecord
  belongs_to :office, primary_key: :office_cd, foreign_key: :company_cd
end
```
上記の例では合計５箇所に`primary_key`と`foreign_key`を書いていますが、一つ一つの設定の役割はなんなのでしょうか。
これらの設定は、いろんな記事でいろんな書き方や説明がされていますが、一つ一つを説明しているものはありませんでした。
また、検索上位にくるような記事でも説明が間違っていることが多く、結構混乱するポイントになってしまっています。
本記事の目標は、一つ一つを理解し、悩むことなく設定できる一助になることです。

## 前提
本記事は`primary_key`と`foreign_key`自体はなんとなく理解されていることを前提とします。

## おことわり
参考になる情報が見当たらず、ほとんど手探りでソースや動作を見てまとめました。
そのため、上で他の記事のこと言っておいてアレですが、この記事にも誤情報紛れ込んでいると思います。
誤りを見つけたらコメントを是非・・・。

## 環境
Rails 7.0.4.2
MySQL: 8.0.32

## テーブル構成
以下のテーブル構成で確認をおこなっていきます。（後半は構成変わっていきます）
![Screenshot 2023-02-07 at 21.12.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474784/54c3b373-7cc6-b714-dfa6-0e565d36639e.png)
1. officesとusersの２つのテーブルがあり、多重度は１：多
1. officesテーブルの主キーは`office_cd`（デフォルトの`id`から変更）
2. usersテーブルの外部キーは`company_cd`（デフォルトの`office_id`から変更）

Rails上のソースはこんな感じ。
```migrate/create_table.rb
    create_table :offices, id: false do |t|
      t.string :office_cd, primary_key: true
      t.string :office_name
      t.timestamps
    end
    create_table :users do |t|
      t.string :company_cd 
      t.string :user_name
      t.timestamps
    end

    # テーブル定義上の外部キーは外しています
    # add_foreign_key :users, :offices, column: :company_cd, primary_key: :office_cd

```

モデルはこんな感じです。
```models.rb
class Office < ApplicationRecord
  has_many :users, foreign_key: :company_cd
end
class User < ApplicationRecord
  belongs_to :office, foreign_key: :company_cd
end
```

foreign_keyにcompany_cdを書いていますが、primary_keyはどこにも書いていません。これは後ほどのprimary_key側の検証でお話しします。

この状態で関連付けができていることを確認します。
```irb.rb
> Office.joins(:users)
  Office Load (1.0ms)  SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`company_cd` = `offices`.`office_cd`
> User.joins(:office)
  User Load (1.5ms)  SELECT `users`.* FROM `users` INNER JOIN `offices` ON `offices`.`office_cd` = `users`.`company_cd`
```
office_cdとcompany_cdで結合しているので、問題なく動いてそうです。


## foreign_keyの役割
それではこの状態から、まずはforeign_keyの役割を確認していきます。
こちらはprimary_keyに比べたらシンプルな話です。

### has_many側のforeign_keyの役割
消した時どのような動きになるか確認してみましょう。
```office.rb
class Office < ApplicationRecord
  # has_many :users, foreign_key: :company_cd
  has_many :users
end
```
↓
```irb.rb
> User.joins(:office) # こっちはOK
  User Load (0.7ms)  SELECT `users`.* FROM `users` INNER JOIN `offices` ON `offices`.`office_cd` = `users`.`company_cd`
> Office.joins(:users)　#こっちはNG
  Office Load (0.5ms)  SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`office_id` = `offices`.`office_cd`
(Object doesn't support #inspect)
=>
```
親のモデルを基準にしたときに、子との関連付けに問題が起きます。
`foreign_key`の設定がないので、デフォルトで`office_id`で結合しようとします。
イメージだとこんな感じ
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474784/2517deac-f144-1547-24f6-2150c84aa83b.png">

### belongs_to側のforeign_keyの役割
こちらは先ほどと逆です。
```user.rb
class User < ApplicationRecord
  belongs_to :office
end
```
↓
```irb.rb
> Office.joins(:users) # こっちはOK
  Office Load (0.7ms)  SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`company_cd` = `offices`.`office_cd`
> User.joins(:office) # こっちはNG
  User Load (0.4ms)  SELECT `users`.* FROM `users` INNER JOIN `offices` ON `offices`.`office_cd` = `users`.`office_id`
(Object doesn't support #inspect)
=>
```
子のモデルを基準にしたときに、親との関連付けに問題が起きます。
`foreign_key`の設定がないので、デフォルトで`office_id`で結合しようとします。


<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474784/a9c3f0f6-2222-e3e4-43ec-7543db6d9684.png">


### foreign_keyまとめ
このように、両方向で関連付けをおこなうために`has_many`にも`belongs_to`にも書いておく必要があります。
外部キーのカラム名をデフォルト（今回だと`office_id`）から変えたい場合は、とりあえず両方に書いておくのが良さそうです。
さて、問題はprimary_keyです。









## primary_keyの役割
foreign_keyを両方に書いたことでお互いの関連付けはうまくいきました。
・・・先ほどからずっと奇妙な動きをしていることにお気づきでしょうか。
primary_keyをどこにも書いていないのに、親のカラムは常にoffice_cdを使っています。こわいですね。

この点は結構ややこしく、以下の流れで整理していきます。
1. なぜクラスに`primary_key`を書いていないのに動くのか
1. なぜassociationに`primary_key`を書いていないのに動くのか
1. クラスの`primary_key`の役割
2. associationの`primary_key`の役割
3. `has_many`と`belongs_to`でのそれぞれの役割

どうしても説明が長くなってしまい、2が特に長いので、飛ばしていただいても大丈夫です。

### 1. なぜクラスに`primary_key`を書いていないのに動くのか

これは、データベース上のテーブル定義から自動でOfficeクラスのprimary_keyが設定されるためです。
```irb.rb
> Office.primary_key
=> "office_cd"
```
Railsの内部ではクラス読み込み時、各DBMSアダプタにある`primary_keys`メソッドで主キーを取得し、primary_keyに設定します。
MySQLの場合は以下のソースです。

https://github.com/rails/rails/blob/73255bae9e2eb1494b01d120530eab6d0b371646/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L573-L573

なので、クラスの`primary_key =`メソッドは、テーブル定義の主キーと同じであれば実は書かなくても動きます。
```office.rb
class Office < ApplicationRecord
  self.primary_key = :company_cd # ←ここの話
  has_many :users, foreign_key: :company_cd
end
```
ただし、ソース側で主キーが`id`ではないことが判断できるので、書いておいた方が親切だとは思います。

### 2. なぜassociationに`primary_key`を書いていないのに動くのか
これも、クラスのprimary_keyをデフォルトで設定してくれるからです。
設定している箇所は`ActiveRecord::Reflection`の中です。全部説明するとめちゃくちゃ長くなるのと、自分もあまり詳しくないので、一部分だけソースを転記していきます。


#### has_manyの場合
`Office.joins(:users)`実行時のSQL
```join.sql
`SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`company_cd` = `offices`.`office_cd`
```
の、結合条件の右辺にoffice_cdを選択する処理部分を見てみます。

```reflection.rb
class AbstractReflection
  ...
  def join_scope(table, foreign_table, foreign_klass)
    ...
    primary_key = join_primary_key # 左辺に何を使うか
    foreign_key = join_foreign_key # 右辺に何を使うか
    
    klass_scope.where!(table[primary_key].eq(foreign_table[foreign_key]))
    ...
    klass_scope
  end
end

class AssociationReflection < MacroReflection
  ...
  # 右辺に何使うの？
  def join_foreign_key
    active_record_primary_key
  end
  # has_manyにprimary_key書いてたらそれを使うけど、入ってないからactive_record(Office)のprimary_keyを使うよ
  def active_record_primary_key
    @active_record_primary_key ||= -(options[:primary_key]&.to_s || primary_key(active_record))
  end
end
```

という流れで、Office.primary_keyが使用されます。

#### belongs_toの場合
`Office.joins(:users)`実行時のSQL
```join.sql
SELECT `users`.* FROM `users` INNER JOIN `offices` ON `offices`.`office_cd` = `users`.`company_cd`
```
の、左辺にoffice_cdを選択する処理部分を見てみます。
join_scopeが動くところまでは一緒です。

```reflection.rb
class BelongsToReflection < AssociationReflection
  ...
  # 左辺に何使うの？
  def join_primary_key(klass = nil)
    polymorphic? ? association_primary_key(klass) : association_primary_key
  end

  # belongs_toにprimary_key書いてたらそれを使うけど、入ってないからself.klass(Office)のprimary_keyを使うよ
  def association_primary_key(klass = nil)
    if options[:foreign_key] && options[:foreign_key].is_a?(Array)
      (klass || self.klass).query_constraints_list
    elsif primary_key = options[:primary_key]
      @association_primary_key ||= -primary_key.to_s
    else
      primary_key(klass || self.klass) # ←　self.klassがOffice
    end
  end
  ...
end
```

Userを基準にしても、associationの動きでOffice側のprimary_keyを見てくれています。

https://github.com/rails/rails/blob/73255bae9e2eb1494b01d120530eab6d0b371646/activerecord/lib/active_record/reflection.rb#L191-L191

ここまでの動きをまとめると、
1. クラスのprimary_keyはRailsがテーブル定義から自動で設定する
1. associationはそれをデフォルトとする
1. primary_keyをどこにも書かなくても動く

となります。

ちなみに、外部キー制約がテーブル定義上にあっても、Railsは自動でforeign_keyを設定してはくれません。
最後に記事を見返しているときにこの可能性に気づきました。もし設定してくれていたら大幅に書き直しでした。


### 3. クラスの`primary_key`の役割
これでprimary_key省略時の動きは分かりました。
では、primary_keyを必ず書く必要があるケースは何があるのでしょうか。

これは、**テーブル定義上の主キーとRails上の主キーを変えたいとき**が考えられます。
ありそうなパターンだと、idは持っているけど別のコード値で扱いたい場合でしょうか。
```create_table.rb
    create_table :offices do |t|
      t.string :office_cd  # テーブル定義ではidが主キーだけど、Rails的にはこっちを主キー扱いにしたい
      t.string :office_name
      t.timestamps
    end
```
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474784/b5306d25-e18c-61d4-e069-9cc4690f2ba3.png">

この場合は、何も書かないとOfficeのprimary_keyは当然`id`になり、結合もそちらを参照します。
```hoge.sql
SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`company_cd` = `offices`.`id`
```

そのため、明示的に書く必要がでてきます。
```office.rb
class Office < ApplicationRecord
  self.primary_key = :company_cd
  has_many :users, foreign_key: :company_cd
end
```

association側のprimary_keyは引き続き書いても書かなくてもよいです。（親クラスのprimary_keyをデフォルトとするため）
この状態で動作を確認してみましょう。こわくなってきたので更新系も一回みておきます。

```irb.rb
> Office.joins(:users)
  Office Load (0.7ms)  SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`company_cd` = `offices`.`office_cd`
> User.joins(:office)
  User Load (1.1ms)  SELECT `users`.* FROM `users` INNER JOIN `offices` ON `offices`.`office_cd` = `users`.`company_cd`
> Office.first.users.create! 
  Office Load (0.8ms)  SELECT `offices`.* FROM `offices` ORDER BY `offices`.`office_cd` ASC LIMIT 1
  TRANSACTION (0.2ms)  BEGIN
  Office Load (0.4ms)  SELECT `offices`.* FROM `offices` WHERE `offices`.`office_cd` = 'C_1' LIMIT 1
  User Create (0.4ms)  INSERT INTO `users` (`company_cd`, `user_name`, `created_at`, `updated_at`) VALUES ('C_1', NULL, '2023-02-11 10:55:03.550294', '2023-02-11 10:55:03.550294')
  TRANSACTION (0.6ms)  COMMIT
```
問題なく動作しています。

### 4. associationの`primary_key`の役割
こちらを書く必要があるケースは**親クラスのprimary_keyとは別のカラムで関連付けたい場合**が考えられます。

```create.rb
create_table :offices do |t|
  t.string :office_cd  # テーブル定義ではidが主キーだけど、Rails的にはこっちを主キー扱いにしたい
  t.string :other_cd # usersとはこちらで関連付けたい
  t.string :office_name
  t.timestamps
end
```
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474784/702018e0-c352-6cb9-b1ab-640bc1b92aec.png">

これも当然指定しないとother_cdで関連付けてはくれません。
ですので、associationに書く必要がでてきます。
```models.rb
class Office < ApplicationRecord
  self.primary_key = :office_cd
  has_many :users, primary_key: :other_cd, foreign_key: :company_cd
end
class User < ApplicationRecord
  belongs_to :office, primary_key: :other_cd ,foreign_key: :company_cd
end
```

動作を確認します。
```irb.rb
> Office.joins(:users)
  Office Load (1.0ms)  SELECT `offices`.* FROM `offices` INNER JOIN `users` ON `users`.`company_cd` = `offices`.`other_cd`
> User.joins(:office)
  User Load (0.9ms)  SELECT `users`.* FROM `users` INNER JOIN `offices` ON `offices`.`other_cd` = `users`.`company_cd
> Office.first.users.create!
  Office Load (0.8ms)  SELECT `offices`.* FROM `offices` ORDER BY `offices`.`office_cd` ASC LIMIT 1
  TRANSACTION (0.1ms)  BEGIN
  Office Load (0.2ms)  SELECT `offices`.* FROM `offices` WHERE `offices`.`other_cd` = 'XXX' LIMIT 1
  User Create (0.4ms)  INSERT INTO `users` (`company_cd`, `user_name`, `created_at`, `updated_at`) VALUES ('XXX', NULL, '2023-02-11 11:33:37.433963', '2023-02-11 11:33:37.433963')
  TRANSACTION (1.0ms)  COMMIT
```

問題なく動きました。

長くなりましたが、ようやく次で最後です。（ここまで見てくれている人が一人でもいることを願います。）

### `has_many`と`belongs_to`でのそれぞれの役割
最後はシンプルな話に戻ります。
foreign_keyは両方向の関連付けのために両方に書いていました。
primary_keyについても、両方向で関連付けを行いたい場合は、両方のassociationにprimary_keyを書いておきましょう。
体力が完全に尽きたので、この章は省略させてください。（foreign_keyの説明を見てね）

## まとめ
基本的には以下の形になるかと思います。

・外部キーのカラム名がデフォルト(xxx_id)と異なる場合→associationの両方にforiegn_keyを書く
・テーブル定義上の主キーと、Rails上の主キーを変えたい場合→親クラスのprimary_keyを書く
・親クラスのprimary_keyとは別のカラムで関連付けたい場合→associationの両方にprimary_keyを書く
・書かないでおくのが不安、書いたほうが分かりやすいなど→とりあえず全部書く

## おわりに
primary_keyの内容を書くのが、想像の20倍ぐらい大変でした。
おかげでArelやRelationにちょっと詳しくなった。