【Rails】1つの入力フォームで複数テーブルにデータを保存する方法【fields_for】


はじめに

通常form_withを使った入力フォームは、フォームに値を入力して送信ボタンを押すことで、入力値が特定のテーブルに保存されます。
すぐわかると思いますが、簡単な例としてコードと動作を載せておきます。

new.html.haml
= form_with(model: @post, local: true) do |f|
  = f.text_field :content
  = f.submit "投稿"


入力内容がpostsテーブルに保存され、トップページにリダイレクトするという動作です。

もしpostsテーブルとは別で投稿内容のタグを保存できるようにしたい場合、postsテーブルに紐づいたtagsテーブルを作成し、そちらにも同時にデータを保存できるようにする必要があります。

完成イメージとテーブルのイメージとしては以下のような感じです。
これぐらいなら同一テーブルでも良さそうですが、簡単な使い方を理解するためにシンプルにしていると思ってください。

postsテーブル

Column Type Options
content string null: false

tagsテーブル

Column Type Options
content string null: false
post_id integer foreign_key

この操作では一つの入力フォームからpostsテーブルとtagsテーブルにそれぞれ保存しています。
どうやって実装すればよいでしょうか?順番にみていきましょう!

tagsテーブルの用意

Tagモデルとテーブルを作っていきます。

ターミナル
$ rails g model tag
マイグレーションファイル
class CreateTags < ActiveRecord::Migration[5.2]
  def change
    create_table :tags do |t|
      t.string :content, null: false  # 追記
      t.references :post, null: false, foreign_key: true  # 追記
      t.timestamps
    end
  end
end
ターミナル
$ rails db:migrate

モデル(Post, Tag)

モデルのファイルに追加します。

post.rb
class Post < ApplicationRecord
  validates :content, presence: true  # 空のデータをはじくバリデーション

  has_many :tags, dependent: :destroy  # アソシエーション + postレコードを削除したときに紐づいたtagを同時に削除
  accepts_nested_attributes_for :tags, allow_destroy: true  # fields_for(後述)に必要
end
tag.rb
class Tag < ApplicationRecord
  validates :content, presence: true  # 空のデータをはじくバリデーション

  belongs_to :post  # アソシエーション
end

これでモデルの準備はできました。

フォーム

続いてフォームを編集しましょう。

new.html.haml
= form_with(model: @post, local: true) do |f|  # postモデルのフォーム作成メソッド
  .post-area
    .post-area__title
      投稿内容
    .post-area__form
    = f.text_field :content  # postモデルのフォームは通常どおり
  = f.fields_for :tags do |tag|  # 別のモデルのフォームはfield_forメソッドを使う
    .tag-area
      .tag-area__title
        タグ
      .tag-area__form
        = tag.text_field :content
  = f.submit "投稿"

fields_forメソッドを使うことで、Postモデルに関連したモデルのフィールドを作成できるようになります。
関連したモデルというのは上で出てきたaccepts_nested_attributes_forを記述したモデルのことです。今回はTagモデルが該当します。

コントローラ

続いてコントローラを編集しましょう!

post_controller.rb
class PostsController < ApplicationController
  # index, edit, update, destroyは省略してます
  def new
    @post = Post.new
    @tag = @post.tags.new
  end

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

  private

  def post_params
    params.require(:post).permit(:content, tags_attributes: [:content, :_destroy, :id])
  end
end

ストロングパラメータ(post_params)内でのtagの書き方が少し特殊になります。
テーブル名_attributes: [:カラム名, :_destroy, :id]という形です。
createアクションのみであれば, :_destroy, :idは不要ですが、これを書くことで更新や削除もいい感じにやってくれます。

これで最初に載せたようなフォームの挙動が再現できるかと思います。

最後に

今回はタグの入力は1つだけでしたが、実際はタグを複数入力できたほうが使いやすいかと思います。JavaScriptを利用すれば、フォームを自由に追加して複数のタグを登録できるのですが、やや複雑なのでそれに関しては別の記事にしたいと思います。

field_forの使い方がなんとなく理解できれば御の字です。
ご意見、ご質問があればコメントよろしくお願いします!