5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

form objectを使ってネストしたフォームにcarrierwaveで画像を保存した話。

Last updated at Posted at 2020-12-31

何をしたか

投稿に紐づく画像(複数)について思うこと

Railsの課題を実施しています。instagramのようなアプリを作っていて、条件は以下の通りです。

  • ユーザーに紐づく投稿を作る
  • 複数画像を投稿できるようにする
  • gem carrierwaveを用いる

当初、この条件を見たときに、carrierwave公式に載っている、複数ファイルアップロードの方法(imageの情報をjson形式で一つのカラムに保存する)を使うことを想定しての課題なんだろうなーと思ったのですが、

(実は、過去にこの実装はやったことがあって、こんな記事も書いていた。)

上記に紹介している私の過去記事中でも引用している、↓こちらの記事を見つけて以来、ちょっとこの方法はよくないのかも、、、と思い出し、やめていたのでした。

ActiveRecord serialize / store の甘い誘惑を断ち切ろう

では、何をしたか

そこで、今回の実装では、このようなDB構造にしました。

Image from Gyazo

ユーザーが投稿をもち、それに紐づく複数のイメージがあるという図になります:relaxed:
そして、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の使い方については、非常に優れた多くの記事があるので多くは書きません。自分が見つけた限りではこちらの記事が一番詳しくてわかりやすいと思いますので、そちらをご覧ください。

【Rails】CarrierWaveチュートリアル

まずは以下のGemをインストールし...

Gemfile
gem 'carrierwave'
gem 'mini_magick' # リサイズ用のgem

インストール後、rails generate uploder Photoで生成したアップローダーは以下の様に記載。

uploaders/photo_uploader.rb
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のモデルファイルには以下の様に記載します。

models/image.rb
class Image < ApplicationRecord
  mount_uploader :photo, ImageUploader
  belongs_to :post
end

これで、carrierwaveで画像を保存するための、基本的な設定はできました。

なお、リサイズ用のメソッドresize_to_fillですが、①mini_magickをインストールしてあり、
かつ②brew install imagemagickimage magickをインストールしていないと動作しません。

imagemagickをRubyで動くようにしたのが、mini_magickという関係があるらしい。)

form objectでフォームを作る

続いて、form objectを用いてpostが投稿されたときに画像も投稿できるようにしたいと思います。まずは1枚投稿できる様にします。

view

views/posts/new.html.slim
= 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

controllers/posts_controller.rb
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

forms/post_form.rb
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

この辺りの実装は、↓こちらを大変参考にさせていただきました。(偶然でしたが、知人の書いた記事でした。ありがたや...:sparkles:)

【Rails】Form Objectを使ってModelに依存しないFormを作成する

余談ですが、私の場合、上記の記事とは違って、form object側でuploaderを呼び出さなくても、問題なく動作したのですよね。。。なぜでしょ:sweat_smile:

複数画像を投稿できる様にする

さて、これで1ファイルが投稿できる様になりました。次は、2ファイルを投稿できる様にします。
ビューファイル・コントローラー・formオブジェクトをそれぞれ以下の様に書き換えます。

view

まずは、file_fieldのオプションに、multiple: trueをつけます。

views/posts/new.html.slim
= form_with model: @post, url: posts_path, local: true do |f|
  = f.label :photoes, '画像'
  = f.file_field :photoes, multiple: true # multiple: trueを追記
  # 後略

そのほか、ラベルやカラムの:photo:photoesに変えていますが、これは必須ではなく、わかりやすさのために変えています。(ただし、この変更はこの後紹介するcontrollerform_objectのコードにも影響しています)

controller

controllerでは受け取る値にphotos: []を指定して、配列を受け取れる様にします。

controllers/posts_controller.rb
class PostsController < ApplicationController
  # 略。ファイル1つの時と変わりありません
  private

  def post_params
    params.require(:post_form).permit(:text, photoes: [])
  end
end

form object

form objectでは、attr_accessorでアクセス可能な値を:photoesに書き換えて、

forms/post_form.rb
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つ作成されていますね:relaxed:

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オブジェクトは、前回作成時にはかなり苦労したのですが、今回はサクッとできました:sparkles:前の時から少し成長できているみたいでよかったです^^
また、偶然ですが、同じ時期にスクールを卒業した人が作成した記事にも助けられて、、、、。自分も頑張らねばと思ったのでした:relaxed:

引き続き、頑張っていきます^^

追伸

このフォームでupdateメソッドも実装したので、紹介します。

form objectでupdate機能を実装した話

5
5
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?