Rails：外部キーのnilを許可する

今回は外部キーのnilを許可する設定にする際、「モデルファイルも設定つけて」というレビューをもらったので、調べたことを深堀りしてみました。

**作成したマイグレーションファイルはこちら**


```ruby
class CreatePortfolios < ActiveRecord::Migration[7.1]
  def change
    create_table :portfolios do |t|
      t.references :user, null: false, foreign_key: true
      t.references :organization, foreign_key: true
      t.string :name, limit: 50, null: false
      t.string :url, limit: 255, null: false
      t.text :introduction
      t.integer :unhealthy_cnt, default: 0, limit: 1
      t.datetime :latest_health_check_time

      t.timestamps
    end

    add_index :portfolios, :url, unique: true
  end
end
```

**モデルファイル**

```ruby
class Portfolio < ApplicationRecord
  belongs_to :user
  belongs_to :organization
  validates :name, presence: true, length: { maximum: 50 }
  validates :url, presence: true, length: { maximum: 255 }, uniqueness: true
  validates :unhealthy_cnt, numericality: { only_integer: true, less_than_or_equal_to: 4 }

  before_destroy :check_user_dependency

  private

  def check_user_dependency
    throw(:abort) if organization.present?
  end
end
```
今回の指摘は`belongs_to :organization`この部分です。

## 環境
Next.js 14.1.4
Rails 7.1.3
Ruby 3.2.3
Docker

## 構成
ユーザーの作成したPortfolio(アプリ)にはoganization(組織)をもたせることができる。
アプリは個人開発したものもあるので、必ずしもoganizationに紐付くわけではない。

この場合、nilを許可する制約を付ける方法は２つあります。

1. allow_nilオプション
2. optional: trueオプション

## allow_nilとは
https://railsguides.jp/active_record_validations.html#allow-nil

`allow_nil`オプションは、バリデーションの一部として使用され、特定のフィールドがnilであることを許可する場合に用います。例えば、以下のように使用します
```ruby
class Article < ApplicationRecord
  belongs_to :author
  validates :title, presence: true, allow_nil: true
end
```
この例では、titleフィールドに対してpresenceバリデーションが適用されていますが、nilの値は許可されています。つまり、titleがnilの場合はバリデーションがスキップされ、それ以外の場合はpresenceバリデーションが適用されます。


## optional: trueとは
optional: trueオプションは、Active Recordの関連（association）で使用され、関連付けられたオブジェクトが存在しないことを許可します。特に、belongs_to関連に対して使用されます。
```ruby
class Article < ApplicationRecord
  belongs_to :author, optional: true
end
```
この例では、authorとの関連付けがオプションであり、articleレコードがauthor_idを持たない（nilである）ことを許可します。これは、ArticleがAuthorに必ずしも属する必要がないことを意味します。

## 違いのまとめ

<dl>
<dt>1.用途</dt>
<dd>•allow_nil: バリデーションの一部として使用され、特定のフィールドがnilであることを許可します。</dd>
<dd>•optional: true: Active Recordの関連に対して使用され、関連付けられたオブジェクトが存在しないことを許可します。</dd>

<dt>2.対象</dt>
<dd>•allow_nil: モデルの特定の属性（フィールド）。</dd>
<dd>•optional: true: belongs_to関連の外部キー。</dd>

<dt>3.適用される場面</dt>
<dd>•allow_nil: 属性に対するバリデーションの際に使用。</dd>
<dd>•optional: true: 関連付けが必須でないことを示すために使用。</dd>
</dl>

　　
**どちらがより適切か**
外部キーがnilでも良い場合には`optional: true`を使用し、特定の条件で外部キーのバリデーションを柔軟に制御したい場合`allow_nil`オプションを使用するのがいいみたい。

今回の例では`optional: true`を使用することにしました。

**修正したコード**
```ruby
  belongs_to :organization, optional: true
```

## 追加で調べたこと
`optional: true`オプションについて、Railsガイドではこの様になっています。

> 4.1.2.11 :optional
:optionalオプションにtrueを指定すると、関連付けられるオブジェクトが存在することが保証されなくなります。このオプションは、デフォルトではfalseです。

https://railsguides.jp/association_basics.html#optional

どうやらこのオプションは、nilを許可するのではなく、関連づいていない状態を許容するというニュアンスらしい。
このニュアンスについて面白い記事を見つけました。

https://qiita.com/ham0215/items/5872d150b3c468dbc4a9

> Hogeテーブルのモデルを作ると下記のようになります。
nilを許可するためにoptional: trueをつけています。
> ```ruby
> class Hoge < ApplicationRecord
>  belongs_to :parent, class_name: 'Hoge', optional: true
> end
> ```
> ただ、optionalの場合はHogesテーブルに存在しないidをparent_idに入れた場合もバリデーションはOKになってしまいます。

記事によると、:optionalオプションは「関連付けがなくても構いませんよ」というだけで、idが存在するかどうかはチェックしてくれないので存在しない外部キーのidが入力された場合も登録できてしまうということのようです。
それを防ぐために、記事では下記のような実装が紹介されています。
```ruby
class Hoge < ApplicationRecord
  belongs_to :parent, class_name: 'Hoge', optional: true
  validates :parent, presence: true, if: :parent_id?
end
```
これだと、allow_nil:オプションを使用したときと変わらないような気がするので、最初から`  validates :parent, presence: true, allow_nil: true`だけで良いような…

おそらくこれは、整合性を保つために行った実装かな？と思いますが、通常はoptional: trueだけで十分みたいです。

> 通常はoptional: trueだけで十分です。allow_nilを使用する場合は、特定のバリデーションロジックが必要な場合に限られます。 GPT


## おわり
長くなりましたが、最後までお付き合いいただきありがとうございました。
ご指摘あればお待ちしております。