何をしたか
投稿に紐づく画像(複数)について思うこと
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.3
Ruby 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
メソッドも実装したので、紹介します。