Ruby on Railsのbelongs_toのバリデーションを外部キーで行うか、アソシエーション名で行うか？


# はじめに

議論の提示です。１つの結論はありません。

# 背景

## モデルの外部キーのバリデーション方法
[Rule #1 to use belongs_to with presence validator – Rails Guides](https://railsguides.net/belongs-to-and-presence-validation-rule1/)の内容です。

### 外部キーによるバリデーション
外部キーによるバリデーションとは

```rb
class User < ActiveRecord::Base
  belongs_to :account
  validates :account_id, :presence => true
end
```

このように、validatesメソッドの第一引数に、外部キーカラム名を指定することです。
この場合、[ActiveRecordモデルにおけるAssociationのバリデーション戦略](https://gist.github.com/juno/651f58134543a4c68fb1)にも

> foo_idのpresenceでは、存在しないIDでも保存できてしまう

と、あるように、存在しない外部キーで保存できます。
例えば、

```rb
User.new(account_id: 999999).valid?
=> true
```

とした時に`999999`がDB上に存在してもしなくても、保存できます。


### アソシエーション名によるバリデーション

アソシエーション名のバリデーションとは、validatesメソッドの第一引数を参照モデル名で指定することです。例えば

```rb
class User < ActiveRecord::Base
  belongs_to :account
  validates :account, :presence => true
end
```

です。この時、AcitveRecordは自動的に、DBを検索してレコードの有無を確認します。

```rb
User.new(account_id: 999999).valid?
=> false
```

## バリデーションのエラー表示

[Rails tip: display association validation errors on fields | IADA webdevelopment](http://iada.nl/blog/article/rails-tip-display-association-validation-errors-fields)の話です。

### バリデーションエラーが表示されない

上記のようにアソシエーション名でのバリデーションは一見良さそうですが、FormBuilderを使ったバリデーションエラーの表示が上手く動きません。

上記の例のモデル

```rb
class User < ActiveRecord::Base
  belongs_to :account
  validates :account, :presence => true
end
```

に対して、次のフォームを作ると

```erb
<div class="field">
    <%= f.label :account %>:<br />
    <%= f.collection_select :account_id, Account.all, :id, :name %>
</div>
```

アカウントを選択しなかった場合に、バリデーションが上手く表示されません。

バリデーションエラーが`account`に対するもので、フォームの要素が`account_id`に対するもので、一致しないからです。

### 対策1

該当ブログで上げらている対応策です。動作検証はしていません。

```rb
class User < ActiveRecord::Base
  belongs_to :account
  validates :account_id, presence: true
  validates :account, presence: true, if: -> { account_id.present? }
end
```

### 対策2

ActionModelまたはActionViewにモンキーパッチを当てる方法が紹介されています。
長いので、[元の記事](http://iada.nl/blog/article/rails-tip-display-association-validation-errors-fields)を読んでください。

### Rails本体の対応状況
元記事は2014年のものです。これをRails本体に取り込めないかというissueが今年の4月に出ています。
[Rails form validation errors not displayed for assocation foreign key field when only presence for the association itself is validated · Issue #28772 · rails/rails](https://github.com/rails/rails/issues/28772)

[PR](https://github.com/rails/rails/pull/28887)もありますが、半年ほど放置されています。

修正内容を見ると、それほど長くはありませんが、ActiveRecordにActiveModelのサブモジュールを追加していて多少こんがらがっています。

- エラー表示はActiveViewの仕事
- エラーの状態管理はActiveModelの仕事
- 外部キーの情報はActiveRecordの持ち物

ということで、実装場所としてはActiveRecordがふさわしいが、ActiveModelへの参照を追加しないといけないので、ちょっとこんがらがった修正が必要なようです。ActiveModelへの参照自体はしているので絶対ダメということはなさそうですが、すぐに取り込まれることはなさそうです。

## 背景のまとめ

1. 外部キーのバリデーションはアソシエーション名でやりたい
2. フォームのバリデーション表示に一工夫必要
3. Rails本体のサポートは見込めない

# 議題 Ruby on Railsのbelongs_toのバリデーションを外部キーで行うか、アソシエーション名で行うか？

次の4パターンに整理します。

|No| 外部キーのバリデーション | フォームのバリデーション表示 | 参照レコードの存在確認 |
|---|---|---|---|
|1| アソシエーション名 | 無視 | ActiveRecordで確認 |
|2| アソシエーション名 | 対応 | ActiveRecordで確認 |
|3| 外部キー | Rails標準で対応可 | しない |
|4| 外部キー | Rails標準で対応可 | RDBの外部キー制約でチェック |


## 外部キーのバリデーションをアソシエーション名 フォームのバリデーションを無視

APIサーバーなどフォームを使わない場合に良さそうです。

## 外部キーのバリデーションをアソシエーション名 フォームのバリデーションを対応

[Rails tip: display association validation errors on fields | IADA webdevelopment](http://iada.nl/blog/article/rails-tip-display-association-validation-errors-fields) の方法を使います。

- 個別のモデルで二重にvalidateする
- モンキーパッチを当てる

モデル数が少ない場合は、個別のモデルvalidateすれば良さそうです。
モデル数が多い場合は、モンキーパッチが欲しいです。ただRailsの方針が読めないのが怖いところです。


## 外部キーのバリデーションを外部キー 参照レコードの存在確認しない

アプリケーションの要件しだいです。
その場合は、外部キーのバリデーションも必要ない気はします。

## 外部キーのバリデーションを外部キー 参照レコードの存在をRDBでする

通常のユーザーインタフェースから存在しない親のidを指定されることは、正常ではないので例外として処理するの妥当に思えます。

# 念のための注意書き

外部キーのバリデーションをアソシエーション名で行う場合でも、DB内のデータの整合性を保証するには外部キー制約が必要です。
手動メンテナンス時に生SQLで不正データを登録されるかもしれません。

# おわりに

他に良いアイデアや、「アソシエーション名でバリデーションする意味ある？」などのご意見があれば、コメント欄なり、はてなブックマークコメントなり、参照記事なり、お待ちしております。

# 参照資料
- [Rule #1 to use belongs_to with presence validator – Rails Guides](https://railsguides.net/belongs-to-and-presence-validation-rule1/)
- [ActiveRecordモデルにおけるAssociationのバリデーション戦略](https://gist.github.com/juno/651f58134543a4c68fb1)
- [Rails tip: display association validation errors on fields | IADA webdevelopment](http://iada.nl/blog/article/rails-tip-display-association-validation-errors-fields)
- [Rails form validation errors not displayed for assocation foreign key field when only presence for the association itself is validated · Issue #28772 · rails/rails](https://github.com/rails/rails/issues/28772)