20
16

More than 3 years have passed since last update.

【Rails】画像を複数投稿したい

Last updated at Posted at 2020-06-14

はじめに

ポートフォリオとして作っていたグループチャットアプリで、画像を複数投稿できるようにしました!

プログラミングを学習し始めて2カ月ほどの初学者ですので、間違っていることもあるかと思います。
ご参考程度に見ていただけると幸いです。
(もっとかっこいいコードの書き方をご存知の方、教えていただけると嬉しいです!)

目指すゴール

目指すのはこ〜んな感じです!
ezgif.com-video-to-gif (9).gif

前提

  1. 画像のアップロードにはgemCarrierWaveMiniMagickを使用。
  2. 画像は複数枚アップロードできる。
  3. テーブル同士の関係は以下の通り。(実際のアプリからは簡略化しています) スクリーンショット 2020-06-14 18.16.05.png

開発環境

ruby 2.5.1
Rails 5.2.4.2
Haml 5.1.2

画像を複数投稿する方法

まず、どのような方法があるか調べました。3つありました。(他にもあるかもです)
1. gemCarrierWaveが用意している"Multiple file uploads"を使う。
2. form objectを使う。
3. **accepts_nested_attributes_forメソッドを使う。**

結論、3を使いました。1と2を使わなかった理由を簡単に説明します。

  1. postsテーブルfilesカラムを作って、そこに配列の形で画像が複数格納される仕様です。画像を1枚ずつ取り出したりするのには向かないかな?と感じたため、他の方法をとることにしました。また、保存した画像がなかなか表示できませんでした。(技術不足です・・・)
    【参考】https://github.com/carrierwaveuploader/carrierwave

  2. 純粋に実装できませんでした:sob:。なぜform objectを使ってみようと思ったかというと、3のaccepts_nested_attributes_forはあまり評判の良くないメソッドであると、いくつかのサイトに書いてあったからです。
    【参考】accepts_nested_attributes_forを使わず、複数の子レコードを保存する
    画像を1枚保存するところまではできましたが複数は実装できず、期日も迫っていたので、3の方法をとることにしました。

Railsの生みの親が"kill"したいらしいaccepts_nested_attributes_forメソッドですが、Railsガイドにバッチリ載ってます!
【参考】公式ドキュメント:Active Record Nested Attributes日本語版はこちら
これも正攻法だ!!と思い直し、このメソッドを使い実装していきますっ

いざ実装

CarrierWaveの導入はこちらをご参考ください▶︎【Rails入門】CarrierWaveを使って画像のアップロードに挑戦!
他にも調べればいっぱいサイトが出てきます。

1. まずはアソシエーションから

早速accepts_nested_attributes_forメソッドの登場です。このメソッドで、postモデルを親モデル、post_fileモデルを子モデルとしたアソシエーションを組むことができます。

post.rb
class Post < ApplicationRecord
  belongs_to :group
  belongs_to :user

  # dependent: :destroyをつけることで、親のレコードが削除された場合に、関連付いている子のレコードも一緒に削除されます。
  has_many :post_files, dependent: :destroy
  accepts_nested_attributes_for :post_files

  validates :content, presence: true
end
post_file.rb
class PostFile < ApplicationRecord
  belongs_to :post

  # 画像アップロードのための記述
  mount_uploader :file, ImageUploader
end

2. ビューに関するコントローラの記述をしていきましょう

ルーティング

グループチャットなので、投稿機能であるpostsgroupsにネストしたルーティングになります。

routes.rb
Rails.application.routes.draw do
  devise_for :users
  root "users#index"
  resources :groups, except: :show do
    resources :posts, only: [:index, :create]
  end
end

コントローラ

postgroupに紐づいているので、before_actiongroup_idを取得しています。
また、今回は投稿を表示するページと新規投稿をするページが同一であるため、newアクションではなくindexアクションPostクラスのインスタンスを作成します。

posts_controller.rb
class PostsController < ApplicationController
  before_action :set_group

  def index
    @post = Post.new
    @post_file = @post.post_files.build
    @posts = @group.posts.includes(:user).order(created_at: "DESC")
  end

  private
  def set_group
    @group = Group.find(params[:group_id])
  end
end

注目は、indexアクションの2行目。
@post.post_files.buildという記述により、Postクラスのインスタンスに関連づけられたPost_fileクラスのインスタンスを作成することができます。

3. 投稿フォームを作っていきましょう

ゴール動画にあるようなフォームを作っていきます。

posts/index.html.haml
.post-form
  = image_tag current_user.avater.url, id: 'avater'
  .input-box
    = form_for [@group, @post] do |f|
      = f.text_area :content, id: 'textarea', class: 'input-box__content', placeholder: '投稿文を入力してください'
      .input-box__bottom
        .input-box__bottom__files
          = f.fields_for :post_files do |i|
            = i.file_field :file, multiple: true, name: "post_files[file][]"
        = f.submit '投稿', class: 'submit-btn'

// 後述のpostの部分テンプレートの呼び出し。groupのpostを全て表示します。
.posts
  = render @posts

ポイントはこの部分!

= f.fields_for :post_files do |i|
  = i.file_field :file, multiple: true, name: "post_files[file][]"

1行目のfields_forは、file_fieldなどと同様にinput要素を生成するフォームヘルパーです。1つのモデル(post)に紐づいた、複数の別のモデル(post_file)を同時に保存したい時に利用できます。
2行目のmultiple: trueで複数画像を選択できるようになります。

4. フォームで入力した値を保存

いよいよデータを保存するための記述です。createアクションを以下のようにします。

posts_controller.rb
class PostsController < ApplicationController
  before_action :set_group

  def index
    @post = Post.new
    @post_file = @post.post_files.build
    @posts = @group.posts.includes(:user).order(created_at: "DESC")
  end

  def create
    @post = @group.posts.new(post_params)
    # 投稿が成功した場合
    if @post.save
      # 画像が投稿されていないパターンもあるので条件分岐
      if params[:post_files].present?
        # フォームで入力されたファイルを一つずつレコードに格納していく
        params[:post_files][:file].each do |a|
          @post_file = @post.post_files.create!(file: a, post_id: @post.id)
        end
      end
      redirect_to group_posts_path(@group)
    # 投稿が失敗した場合
    else
      @posts = @group.posts.includes(:user).order(created_at: "DESC")
      render :index
    end
  end

  private
  def post_params
    params.require(:post).permit(:content, post_files_attributes: [:file]).merge(user_id: current_user.id)
  end

  def set_group
    @group = Group.find(params[:group_id])
  end
end

ポイントはストロングパラメータ

  private
  def post_params
    params.require(:post).permit(:content, post_files_attributes: [:file]).merge(user_id: current_user.id)
  end

fields_forを使ったフォームから送信される値は、post_files_attributes: [:file]のような形でparamsに入ります。

5. 保存はできた!最後に表示だ!!

なぜこんなにも意気込んでいるかというと、ここでハマったからです笑
保存したデータを表示しましょう。
メッセージは保存しているだけ表示するので、部分テンプレートで切り出します。
(ゴール動画では、コメントするやら見ましたやらついていますが、以下コードではわかりやすさのため省略してます。)

_post.html.haml
.post
  .top
    = image_tag post.user.avater.url, id: 'avater'
    .top__right
      .top__userinfo
        .top__userinfo--user-name
          = post.user.name
        .top__userinfo--datetime
          = l post.created_at, format: :datetime
  .middle
    .post-content
      // text_areaで入力された改行をそのまま表示するために、simple_formatを使用
      = simple_format(post.content)
    // 画像があれば表示
    - if post.post_files.present?
      .post-files
        // postに紐ずくpost_filesをeachで一つずつ取り出して表示
        - post.post_files.each do |file|
          // objectタグで書くことでpdfとかも表示できます。(画像だけの場合はimage_tagを使えば良いと思います。)
          %object{data: file.file.url, class: 'post-files__file'}

middleクラスの部分だけcssも載っけておきます。

post.sass
.middle {
  padding: 10px 10px 10px 50px;
  .post-content {
    margin-bottom: 10px;
  }
  .post-files {
    // 横並び
    display: flex;
    // この2行で枠の幅の中で要素を折り返し表示
    flex-direction: row;
    flex-wrap: wrap;
    margin-bottom: 10px;
    &__file {
      width: 150px;
      height: 150px;
      margin: 5px;
      border: 1px solid #ccc;
      // 画像の縦横比率を変えずに表示
      object-fit: contain;
    }
  }
}

ハマったのはもちろんここ。

- if post.post_files.present?
  .post-files
    - post.post_files.each do |file|
      %object{data: file.file.url, class: 'post-files__file'}  <---ここ!!

最初にどう記述していたかといいますと、以下です。

      %object{data: file.url, class: 'post-files__file'}

この記述だと、
NoMethodError undefined method 'url' for #<PostFile:0x00007fd979263950>
ってなります。
原因は、eachで一つずつ取り出したpost_filesはカラム名までしっかり指定してあげる必要があるからです。

- post_files(postと紐ずくファイルたち)
  - file (eachで一つずつ取り出したfile)
     - file(カラム名) <-----ここに画像データが保存されている!
     - post_id (カラム名)
  - file
     - file
     - post_id

という感じになっているので、file.file.urlと記述する必要があります。

これで画像複数投稿機能の完成です!

画像を複数投稿後の表示に関わるコードは載っていないサイトも多く、ここまで表示に情熱を注いだブログもないのではないかと・・・(ゆえに調べてもなかなか解決せず苦労しました。)
form objectでも実装できるようになりたいです!

次は、この複数投稿をajaxで表示させる方法を投稿したいと思います。
最後までお読みいただきありがとうございました!

参考サイト

20
16
2

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
20
16