何をしたか
投稿に紐づく画像(複数)について思うこと
Railsの課題を実施しています。instagramのようなアプリを作っていて、条件は以下の通りです。
- ユーザーに紐づく投稿を作る
- 複数画像を投稿できるようにする
-
gem carrierwaveを用いる
当初、この条件を見たときに、carrierwave公式に載っている、複数ファイルアップロードの方法(imageの情報をjson形式で一つのカラムに保存する)を使うことを想定しての課題なんだろうなーと思ったのですが、
(実は、過去にこの実装はやったことがあって、こんな記事も書いていた。)
上記に紹介している私の過去記事中でも引用している、↓こちらの記事を見つけて以来、ちょっとこの方法はよくないのかも、、、と思い出し、やめていたのでした。
ActiveRecord serialize / store の甘い誘惑を断ち切ろう
では、何をしたか
そこで、今回の実装では、このようなDB構造にしました。
ユーザーが投稿をもち、それに紐づく複数のイメージがあるという図になります![]()
そして、accept_nested_attributes_forを使うと楽なんだろうなあ。。。。と思いつつ、こちらもあまりよくない実装と聞いていたので、復習も兼ねてform_objectを使うことにしました。
↓参考までに、過去記事を紹介します。
▼accept_nested_attributes_forを使ったフォームは実装したことがあった
fields_forで子テーブルのデータを一気に作成する(テストも書いてます)[Rails][Rspec]
▼form_objectも作ったことがあったけど、よくわかっていなかった
form_objectで親子関係のあるフォームを作成する(テストも書いてます)
実装
それでは、早速実装していこうと思います。なお、実行環境は以下の通りです。
Rails 5.2.3Ruby 2.6.0
CarrierWaveで画像を投稿できるようにする
Carrierwaveの使い方については、非常に優れた多くの記事があるので多くは書きません。自分が見つけた限りではこちらの記事が一番詳しくてわかりやすいと思いますので、そちらをご覧ください。
まずは以下のGemをインストールし...
gem 'carrierwave'
gem 'mini_magick' # リサイズ用のgem
インストール後、rails generate uploder Photoで生成したアップローダーは以下の様に記載。
class PhotoUploader < CarrierWave::Uploader::Base
# デフォルトでついてくるコメント類は削除しています。また、最低限のみ残した記述です。
include CarrierWave::MiniMagick
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
process resize_to_fill: [400, 300]
def extension_whitelist
%w(jpg jpeg gif png)
end
end
Imageのモデルファイルには以下の様に記載します。
class Image < ApplicationRecord
mount_uploader :photo, ImageUploader
belongs_to :post
end
これで、carrierwaveで画像を保存するための、基本的な設定はできました。
なお、リサイズ用のメソッドresize_to_fillですが、①mini_magickをインストールしてあり、
かつ②brew install imagemagickでimage magickをインストールしていないと動作しません。
(imagemagickをRubyで動くようにしたのが、mini_magickという関係があるらしい。)
form objectでフォームを作る
続いて、form objectを用いてpostが投稿されたときに画像も投稿できるようにしたいと思います。まずは1枚投稿できる様にします。
view
= form_with model: @post, url: posts_path, local: true do |f|
= f.label :photo, '画像'
= f.file_field :photo
= f.label :text, 'テキスト'
= f.text_field :text'
= f.submit '投稿する'
controller
class PostsController < ApplicationController
def new
@post = PostForm.new(user_id: current_user.id)
end
def create
@post = PostForm.new(post_params.merge(user_id: current_user.id))
if @post.save!
# 投稿が成功したときの処理
else
# 投稿が成功しなかったときの処理
end
end
private
def post_params
params.require(:post_form).permit(:text, :photo)
end
end
form object
class PostForm
include ActiveModel::Model
def initialize(params)
super(params)
end
attr_accessor :text, :photo, :user_id
validates :text, presence: true
def save!
return false if invalid?
post = Post.new(text: text, user_id: user_id)
post.images.build(photo: photo).save!
post.save! ? true : false
end
end
この辺りの実装は、↓こちらを大変参考にさせていただきました。(偶然でしたが、知人の書いた記事でした。ありがたや...
)
【Rails】Form Objectを使ってModelに依存しないFormを作成する
余談ですが、私の場合、上記の記事とは違って、form object側でuploaderを呼び出さなくても、問題なく動作したのですよね。。。なぜでしょ![]()
複数画像を投稿できる様にする
さて、これで1ファイルが投稿できる様になりました。次は、2ファイルを投稿できる様にします。
ビューファイル・コントローラー・formオブジェクトをそれぞれ以下の様に書き換えます。
view
まずは、file_fieldのオプションに、multiple: trueをつけます。
= form_with model: @post, url: posts_path, local: true do |f|
= f.label :photoes, '画像'
= f.file_field :photoes, multiple: true # multiple: trueを追記
# 後略
そのほか、ラベルやカラムの:photoを:photoesに変えていますが、これは必須ではなく、わかりやすさのために変えています。(ただし、この変更はこの後紹介するcontrollerとform_objectのコードにも影響しています)
controller
controllerでは受け取る値にphotos: []を指定して、配列を受け取れる様にします。
class PostsController < ApplicationController
# 略。ファイル1つの時と変わりありません
private
def post_params
params.require(:post_form).permit(:text, photoes: [])
end
end
form object
form objectでは、attr_accessorでアクセス可能な値を:photoesに書き換えて、
class PostForm
include ActiveModel::Model
# 前略
attr_accessor :text, :photoes, :user_id
# 略
def save!
return false if invalid?
post = Post.new(text: text, user_id: user_id)
photoes.each do |photo|
post.images.build(photo: photo).save!
end
post.save! ? true : false
end
end
photoesそれぞれに対しsave!メソッドを回しました。
完成
上記で作成されたレコードが下記の通りです。きちんと別のファイルで投稿に紐づく画像が2つ作成されていますね![]()
3] pry(main)> post = Post.last
Post Load (1.0ms) SELECT `posts`.* FROM `posts` ORDER BY `posts`.`id` DESC LIMIT 1
=> #<Post:0x00007f9296488228
id: 14,
body: "2ファイル投稿のテスト",
user_id: 1,
created_at: Thu, 31 Dec 2020 05:57:09 UTC +00:00,
updated_at: Thu, 31 Dec 2020 05:57:09 UTC +00:00>
[4] pry(main)> post.images
Image Load (0.7ms) SELECT `images`.* FROM `images` WHERE `images`.`post_id` = 14
=> [#<Image:0x00007f929a502da8
id: 14,
photo: "sample_01.jpg",
post_id: 14,
created_at: Thu, 31 Dec 2020 05:57:09 UTC +00:00,
updated_at: Thu, 31 Dec 2020 05:57:09 UTC +00:00>,
#<Image:0x00007f929a502ad8
id: 15,
photo: "sample_02.jpg",
post_id: 14,
created_at: Thu, 31 Dec 2020 05:57:09 UTC +00:00,
updated_at: Thu, 31 Dec 2020 05:57:09 UTC +00:00>]
感想など
formオブジェクトは、前回作成時にはかなり苦労したのですが、今回はサクッとできました
前の時から少し成長できているみたいでよかったです^^
また、偶然ですが、同じ時期にスクールを卒業した人が作成した記事にも助けられて、、、、。自分も頑張らねばと思ったのでした![]()
引き続き、頑張っていきます^^
追伸
このフォームでupdateメソッドも実装したので、紹介します。
