fields_forとaccepts_nested_attributes_forでネストされたフォームを簡単に扱う方法と注意点

# はじめに
日々勉強中の駆け出しWebエンジニアです。最近のプロジェクトで多用している`fields_for`について学んだことをまとめようと思います。
Railsのフォームを作成する際に、ネストしたモデルを扱う場面で登場する`fields_for`の使い方で、フォームが表示されなくなったり、ストロングパラメータの設定で悩むことがあったので、これから学ぶ方の助けになれば幸いです。

# fields_forとは
`fields_for`は、Railsのフォームビルダーの一部であり、親モデルにネストされた子モデルに対するフォーム要素を作成するために使用されます。`form_with`などとは異なり、fields_forは親モデル内で関連する子モデルに対してのフォームを生成できます。

例えば、ある親モデル（Post）が子モデル（Comment）を持つ場合、投稿フォーム内にコメントフォームを作成することができます。これにより、1つのフォームでPostとCommentの両モデルを更新することができます。

基本的な使い方としては、form_withのブロック内でfields_forを使用します。
```
<%= form_with model: @post do |f| %>
  <%= f.text_field :title %>

  <%= f.fields_for :comments do |c_form| %>
    <%= c_form.text_field :content %>
  <% end %>

  <%= f.submit %>
<% end %>
```
この例では、`@post`という親モデルのフォームの中で、`comments`という子モデルのフォームも含まれています。

# 実装手順
filds_forを使って親モデルと子モデルを更新するフォームの実装手順について説明していきます

## Nested attributesを使ってネストされたモデルを設定
fields_forを使うためには、親モデルと子モデルが正しく関連付けられている必要があります。
また、親モデルには`accepts_nested_attributes_for`を記述する必要があります。
```
# 親モデル（Post）
class Post < ApplicationRecord
  has_many :comments
  accepts_nested_attributes_for :comments
end

# 子モデル (Comment)
class Comment < ApplicationRecord
  belongs_to :post
end
```
ここで`accepts_nested_attributes_for :comments`があることで、PostモデルのフォームからCommentモデルの属性を受け取って保存することが可能になります。

### accepts_nested_attributes_for のオプション
`accepts_nested_attributes_for`には、フォームの挙動を柔軟に行える便利なオプションがあります。

#### 1. allow_destroy
allow_destroyをtrueに設定すると、子モデルのレコードをフォームから削除できるようになります
```
accepts_nested_attributes_for :comments, allow_destroy: true
```
#### 2. reject_if
このオプションを使うと、特定の条件（コメントの空の場合など）に合致する子モデルのレコードを無視することができます。これにより空のコメントフィールドがあっても、無意味なレコードが作成されることを防ぎます
```
accepts_nested_attributes_for :comments, reject_if: proc { |attributes| attributes['content'].blank? and attributes['title'].blank? }
```
#### 3. update_only
通常、Railsは新規レコードの作成か既存レコードの更新かを自動的に判断しますが、このオプションをtrueに設定すると、既存レコードを必ず更新し、新しいレコードは作成しなくなります。

```
accepts_nested_attributes_for :comments, update_only: true
```

## viewにフォームを実装
次に、フォームのviewを実装します。

ここで`fields_for`を使って、`Post`と`Comment`の両方のデータを1つのフォームで送信できるようにします。

```
<%= form_with model: @post do |f| %>
  <%= f.text_field :title %>

  <%= f.fields_for :comments do |comment_form| %>
    <%= comment_form.text_field :content %>
  <% end %>

  <%= f.submit "Save Post" %>
<% end %>
```
このフォームでは、`@post`という親モデルのタイトルフィールドと、ネストされた`comments`のフィールドが一緒に表示されます。

## controllerでストロングパラメータを設定
ネストされたモデルの属性を受け取るには、ストロングパラメータで`nested_attributes`を許可する必要があります。これを忘れると、フォームから送信されたデータが適切に処理されなくなります。
```
def post_params
  params.require(:post).permit(:title, comments_attributes: [:content])
end
```

# 注意点
`fields_for`を使用するにあたり引っかかったポイントがあるので紹介します。

## 子モデルが存在しない場合にフォームが表示されない
`fields_for`を使う際に注意すべきポイントの一つとして、子モデル（Commentなど）が存在しない場合、フォームが表示されないという問題があります。

これを回避するには、親モデルに空の子モデルをあらかじめ用意しておく必要があります。
```
class PostsController < ApplicationController
  def new
    @post = Post.new
    @post.comments.build # 空のCommentオブジェクトを作成
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post
    else
      render :new
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, comments_attributes: [:content])
  end
end
```
このようにして、子モデルがなくてもフォームに必ず1つのコメントフィールドが表示されるようになります。

# 最後に
`fields_for`を使うことで、親モデルと子モデルを一括で管理するフォームを作成することができます。
そして`reject_if`、`update_only`などのオプションをうまく活用することで、不要なレコードの作成や削除を防ぎ、フォームの挙動を制御できます。

最初は少し複雑に感じるかもしれませんが、使いこなすと非常に便利な機能です。