Edited at

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

More than 1 year has passed since last update.


はじめに

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


背景


モデルの外部キーのバリデーション方法

Rule #1 to use belongs_to with presence validator – Rails Guidesの内容です。


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

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

class User < ActiveRecord::Base

belongs_to :account
validates :account_id, :presence => true
end

このように、validatesメソッドの第一引数に、外部キーカラム名を指定することです。

この場合、ActiveRecordモデルにおけるAssociationのバリデーション戦略にも


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


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

例えば、

User.new(account_id: 999999).valid?

=> true

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


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

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

class User < ActiveRecord::Base

belongs_to :account
validates :account, :presence => true
end

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

User.new(account_id: 999999).valid?

=> false


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

Rails tip: display association validation errors on fields | IADA webdevelopmentの話です。


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

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

上記の例のモデル

class User < ActiveRecord::Base

belongs_to :account
validates :account, :presence => true
end

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

<div class="field">

<%= f.label :account %>:<br />
<%= f.collection_select :account_id, Account.all, :id, :name %>
</div>

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

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


対策1

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

class User < ActiveRecord::Base

belongs_to :account
validates :account_id, presence: true
validates :account, presence: true, if: -> { account_id.present? }
end


対策2

ActionModelまたはActionViewにモンキーパッチを当てる方法が紹介されています。

長いので、元の記事を読んでください。


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

PRもありますが、半年ほど放置されています。

修正内容を見ると、それほど長くはありませんが、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 の方法を使います。


  • 個別のモデルで二重にvalidateする

  • モンキーパッチを当てる

モデル数が少ない場合は、個別のモデルvalidateすれば良さそうです。

モデル数が多い場合は、モンキーパッチが欲しいです。ただRailsの方針が読めないのが怖いところです。


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

アプリケーションの要件しだいです。

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


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

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


念のための注意書き

外部キーのバリデーションをアソシエーション名で行う場合でも、DB内のデータの整合性を保証するには外部キー制約が必要です。

手動メンテナンス時に生SQLで不正データを登録されるかもしれません。


おわりに

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


参照資料