はじめに
Rails で、 DB と連携していない model のオブジェクトを入力フォームで取り扱い、バリデーションなども含めたいという場合に、 ActiveModel::Model を include した model を Form オブジェクトとして取り扱う、というケースは多いと思います。
その中で、親子関係を持った Form オブジェクトを扱うときにどのように実装するか考える機会があったので、こちらの記事にまとめます。
環境前提
- rails 5.2.2
- ruby 2.5.1
- enumerize 2.2.2
- simple_form 4.0.1
- hamlit-rails 0.2.0
tl;dr
form_for で fields_for メソッドを使えるよう、必要なメソッドを model に定義すれば実現可能
サンプル
以下のような簡単なサンプルを Rails の scaffold を使って作りました。
- Post としてタイトル・本文が入力可能
- Tag を 3 つ指定して登録
- 保存するとタイトル・本文・タグをまとめた文字列を表示
新規登録画面
一覧画面
サンプルの前提
- model は
Post、Tagの 2 種類 -
Posthas manyTagという関係 -
PostはApplicationRecordを継承 -
TagはApplicationRecordを継承せず、ActiveModel::Modelを include した model
基本方針
Rails API ドキュメント の fields_for の説明を踏まえると、fields_for メソッドで has many な子となる Tag model を扱うためには、親となる Post model に以下 2 つのメソッドを定義する必要があります。
-
tagsという reader メソッドを定義 -
tags_attributesという writer メソッドを定義
どちらも ApplicationRecord なオブジェクトであれば、 accepts_nested_attributes_for を利用できたりしますが、今回は子の model が ActiveModel::Model を include した model ですので利用できません。。
今回やりたいことを実現するには、上記 2つのメソッドが不可欠ですので、こちらをもとに実装しました。
ソースコード
こちらにサンプルコードをアップしています。
model
Post
親となる Post model に必要なメソッドを定義しています。
また、新規登録時に複数 Tag を表示できるよう、build_tags メソッドと attr_writer で定義した tags の writer メソッドで、複数 Tag オブジェクトを生成できるようにしています。
あとは Form オブジェクトの値を扱う例として、set_tag_string メソッドで Tag の値を1つの文字列にまとめて、 tag_string カラムに保存できるようにしました。
class Post < ApplicationRecord
before_save :set_tag_string
attr_writer :tags
def tags
@tags ||= []
end
def tags_attributes=(attributes)
@tags = attributes.map do |i, attribute|
Tag.new(attribute)
end
end
def build_tags
self.tags = [Tag.new] * 3
end
def set_tag_string
self.tag_string = self.tags.map(&:name).join(' / ')
end
end
Tag
Tag の方は特に難しいことはしておらず、タグのセレクトボックスに使うリストを enumerize メソッドで定義しただけです。
class Tag
include ActiveModel::Model
extend Enumerize
attr_accessor :name
enumerize :name, in: %i(personal business family)
end
Controller
model 側で fields_for メソッドを定義したことにより、post_params の中で、tags_attributes を指定できるようになっています。
class PostsController < ApplicationController
before_action :set_post, only: %i(show edit update destroy)
def index
@posts = Post.all
end
def show
end
def new
@post = Post.new
@post.build_tags
end
def edit
end
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: 'Post was successfully created.' }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @post.update(post_params)
format.html { redirect_to @post, notice: 'Post was successfully updated.' }
format.json { render :show, status: :ok, location: @post }
else
format.html { render :edit }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
def destroy
@post.destroy
respond_to do |format|
format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(
:titie, :content, tags_attributes: [:name]
)
end
end
View
simple_form でフォームを簡略化しています。
simple_form で fields_for に対応するメソッドは simple_fields_for メソッドですが、今回は fields_for メソッドの利用確認をしたかったので、fields_for メソッドを使っています。
= simple_form_for @post do |f|
= f.error_notification
= f.input :titie
= f.input :content
= f.fields_for :tags do |tf|
= tf.input :name
= f.submit
おわりに
ApplicationRecord でない Form オブジェクトのモデルでも、fields_for メソッドを使えるようになるのは便利だなと思いました。
accepts_nested_attributes_for に頼らなくても親子関係の model を fields_for で扱えるようになるということを知っているだけでも、実装の幅が広がるなと思います。

