かなり丁寧に書いたのでボリューム多めです。
分かっている部分はガンガン読み飛ばしてください
やりたいこと
・accepts_nested_attributes_for
を使わず複数のモデルを操作したい。
・データの新規作成だけでなく、更新もしたい。
事前準備
accepts_nested_attributes_forとは?
関連付けられたモデルのレコードを一度に更新できるメソッドです。
上記の場合だと、メッセージとそれに紐づいた画像を保存できます。便利ですね。
ただ、実はこのメソッドはあまり評判が良くありません(詳しくはこちら)
そこで、代替案のフォームオブジェクトを紹介していきます。
フォームオブジェクトとは?
モデルから切り離され、フォームの処理用に独立したクラスです。
これを使うと**accepts_nested_attributes_for
なしで複数のレコードを更新できます。**
その他メリットや原理について詳しく知りたい方はこちらの記事を。説明がめちゃくちゃ分かりやすいです
本記事の目標
ユーザー(親)がメッセージ(子)に画像(孫)を紐づけて送信できる機能を実装します。
親、子、孫モデルは皆さんの状況に合わせて変えてください。
割愛した部分
・ルーティング設定
・画像投稿に必要なcarrierwaveの設定
・各モデルのテーブル作成
実装例
それでは見ていきましょう!
モデル
class User < ApplicationRecord
has_many :messages
end
class Message < ApplicationRecord
has_many :pictures
belongs_to :user
# ここに書いていたフォームのバリデーションは、フォームオブジェクトに移ります。
end
class Picture < ApplicationRecord
belongs_to :message
# 画像投稿しない方は、以下の記述は不要です。
mount_uploader :picture, PictureUploader
end
バリデーションに関する記述がなくなりました。
スッキリして読みやすいですね
コントローラ
ここで注目して欲しいのは、下記の2点だけです。
・assigns_attributes
でパラメータから渡された値をもとに@message
の情報を更新。
・MessageForm
は、後ほど解説するフォームオブジェクト用のクラス。
# 新規作成画面
def new
@message = MessageForm.new
end
# 新規作成
def create
@message = MessageForm.new
@message.assign_attributes(message_form_params)
if @message.save
# 成功・失敗の処理
end
end
# 編集画面
def edit
# params[:id]の部分は、編集するオブジェクトを特定できる値を入れてください
@message = MessageForm.new(message = Message.find(params[:id]))
end
# 編集
def update
@message = MessageForm.new(message = Message.find(params[:id]))
@message.assign_attributes(message_form_params)
if @message.save
# 成功・失敗の処理
end
end
private
# ストロングパラメータ
def message_form_params
params.require(:message_form).permit(:body, pictures_attributes: [:picture]).merge(user_id: current_user)
end
ストロングパラメータのpictures_attributes
については、次に説明していきます。
ビュー
メッセージの新規作成画面です。
<%= form_with model: @message, url: メッセージ作成用のpath, local: true do |f| %>
<%= f.text_area :body %>
<%= f.fields_for :pictures do |picture| %>
<%= picture.file_field :picture, multiple: "multiple", name: "message_form[pictures_attributes][][picture]" %>
<% end %>
<%= f.submit "送信" %>
<% end %>
パラメータを中心に解説します。
まず最初に、テキストエリアに「OK」と入力して送信したとします。
このときのパラメータは下記のようになります。(ターミナルで確認してみてください。)
Parameters: {"authenticity_token"=>"略", "message_form"=>{"body"=>"OK"}, "button"=>""}
bodyカラムに入力した値が入っていますね。
ここで、"message_form"
となっているのは@message
がMessageForm
クラスから生成しているからです。(自動でこうなります。)
次に、これに画像情報を追加するなら、下記のような形にしたいですよね。
Parameters: {"authenticity_token"=>"略", "message_form"=>{画像情報, "body"=>"OK"}, "button"=>""}
そこで、file_field
のname属性を使ってみます。
name: "message_form[pictures_attributes][][picture]"
こう書くと、画像を2枚投稿した場合のパラメータは下記になります。
(画像情報は長いので省略しています。)
Parameters: {"authenticity_token"=>"略", "message_form"=>{"pictures_attributes"=>[{"picture"=>1枚目の画像情報}, {"picture"=>2枚目の画像情報}], "body"=>"OK"}, "button"=>""}
"message_form"
下に画像情報が入ったのが分かると思います。
また、"pictures_attributes"=>[{"picture"=>
がコントローラのストロングパラメータに対応しています。
ひとまず、これでコントローラに値を渡せそうですね
ちなみに、編集画面の場合は注意が必要です。
理由はこちらの記事に書いていますので、時間があれば参考にしてください。
フォームオブジェクト用クラス
ここが一番ややこしい部分です。
4つに分けて解説しますので、気楽に読んでください
class MessageForm
# part-1
include ActiveModel::Model
include Virtus.model
extend CarrierWave::Mount
validates :body, presence: true
attribute :body, String
attribute :user_id, Integer
mount_uploader :picture, PictureUploader
attr_accessor :pictures
# part-2
def initialize(message = Message.new)
@message = message
self.attributes = @message.attributes if @message.persisted?
end
# part-3
def assign_attributes(params = {})
@params = params
pictures_attributes = params[:pictures_attributes]
@pictures ||= []
pictures_attributes&.map do |pictures_attribute|
picture = Picture.new(pictures_attribute)
@pictures.push(picture)
end
@params.delete(:pictures_attributes)
@message.assign_attributes(@params) if @message.persisted?
super(@params)
end
# part-4
def save
return false if invalid?
if @message.persisted?
@message.pictures = pictures if pictures.present?
@message.save!
else
message = Message.new(user_id: user_id,
body: body)
message.pictures = pictures if pictures.present?
message.save!
end
end
end
part-1(モジュールの導入)
include ActiveModel::Model
include Virtus.model
extend CarrierWave::Mount
validates :body, presence: true
attribute :body, String
attribute :user_id, Integer
mount_uploader :picture, PictureUploader
attr_accessor :pictures
ここはインクルードしたモジュールなどをまとめています。
・ActiveModel::Model
は、MessageForm
オブジェクトを保存する際にバリデーションを使えるようにしています。validates
の部分。ActiveRecord
を継承していなくてもバリデーションできるのが便利。
・Virtus.model
は、このクラスの属性名と型を定義しています。attribute
の部分。
何をパラメータとして受け取るのか明示しています。ちなみに、gemのvirtusをインストールしています。
・CarrierWave::Mount
は画像のアップロード用です。mount_uploader
の部分。
attr_accessor
はメッセージに画像情報をセットするためのものです。(part-4で使います。)
part-2(オブジェクトの初期化)
オブジェクトにプロパティを持たせます。
def initialize(message = Message.new)
@message = message
self.attributes = @message.attributes if @message.persisted?
end
ここはinitialize
の引数がポイント。
コントローラのnew
メソッドに引数がある場合、それを受け取っています。
def edit
@message = MessageForm.new(message = Message.find(params[:id]))
end
つまり、new
でオブジェクトを生成した際に、すでに作成したメッセージの情報があれば、それをもとにフォームオブジェクトの属性を書き換えています。
(self.attributes
の部分)
part-3(オブジェクトの更新)
そろそろ疲れてきた方はすみません。もう少しの辛抱です。。。
def assign_attributes(params = {})
@params = params
pictures_attributes = params[:pictures_attributes]
# picturesと書けば、画像情報が呼び出せる
@pictures ||= []
pictures_attributes&.map do |pictures_attribute|
picture = Picture.new(pictures_attribute)
@pictures.push(picture)
end
# パラメータから画像情報を削除
@params.delete(:pictures_attributes)
# すでに作成したメッセージの情報を更新
@message.assign_attributes(@params) if @message.persisted?
# フォームオブジェクトの情報を更新
super(@params)
end
ここではassign_attributes
で@message
にパラメータの値をセットしています。
assign_attributes
メソッドはもともとデータ更新用のメソッドなのですが、
・フォームオブジェクトが持つ編集用の情報を、すでに作成したメッセージに反映させたい
・pictures
と書けば画像情報を取得orセットできるようにしたい(part-2のattr_accessor
)
という理由から少し改造しています
part-4(変換作業)
いよいよラスト!Message
モデルへの変換作業です。
def save
# 保存前にバリデーションをかける
return false if invalid?
# 編集する場合の処理
if @message.persisted?
@message.pictures = pictures if pictures.present?
@message.save!
else
# 新規作成の場合の処理
message = Message.new(user_id: user_id,
body: body)
message.pictures = pictures if pictures.present?
message.save!
end
end
編集の場合は、普通の保存と変わりないですね。
このときの@message
は、part-2のmessage
と同じです。
大事なのは、下記のMessage
オブジェクトへの変換。
message = Message.new(user_id: user_id,
body: body)
フォームオブジェクトが持ってきた値をカラムに渡しています。
これでMessageForm
からMessage
オブジェクトへの変換が完了しました。
message.pictures = pictures if pictures.present?
最後に、画像情報をメッセージにセットして終了です。
お疲れ様でした
環境
ruby: 2.7.1
rails: 6.0.3.3