はじめに
ポートフォリオとして作っていたグループチャットアプリで、画像を複数投稿できるようにしました!
プログラミングを学習し始めて2カ月ほどの初学者ですので、間違っていることもあるかと思います。
ご参考程度に見ていただけると幸いです。
(もっとかっこいいコードの書き方をご存知の方、教えていただけると嬉しいです!)
目指すゴール
前提
- 画像のアップロードにはgemのCarrierWaveとMiniMagickを使用。
- 画像は複数枚アップロードできる。
- テーブル同士の関係は以下の通り。(実際のアプリからは簡略化しています)
 
開発環境
ruby 2.5.1
Rails 5.2.4.2
Haml 5.1.2
画像を複数投稿する方法
まず、どのような方法があるか調べました。3つありました。(他にもあるかもです)
- 
gemのCarrierWaveが用意している"Multiple file uploads"を使う。
- 
form objectを使う。
- **accepts_nested_attributes_forメソッドを使う。**
結論、3を使いました。1と2を使わなかった理由を簡単に説明します。
- 
postsテーブルにfilesカラムを作って、そこに配列の形で画像が複数格納される仕様です。画像を1枚ずつ取り出したりするのには向かないかな?と感じたため、他の方法をとることにしました。また、保存した画像がなかなか表示できませんでした。(技術不足です・・・)
 【参考】https://github.com/carrierwaveuploader/carrierwave
- 
純粋に実装できませんでした  。なぜ 。なぜ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モデルを子モデルとしたアソシエーションを組むことができます。
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
class PostFile < ApplicationRecord
  belongs_to :post
  # 画像アップロードのための記述
  mount_uploader :file, ImageUploader
end
2. ビューに関するコントローラの記述をしていきましょう
ルーティング
グループチャットなので、投稿機能であるpostsをgroupsにネストしたルーティングになります。
Rails.application.routes.draw do
  devise_for :users
  root "users#index"
  resources :groups, except: :show do
    resources :posts, only: [:index, :create]
  end
end
コントローラ
postはgroupに紐づいているので、before_actionでgroup_idを取得しています。
また、今回は投稿を表示するページと新規投稿をするページが同一であるため、newアクションではなくindexアクションでPostクラスのインスタンスを作成します。
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. 投稿フォームを作っていきましょう
ゴール動画にあるようなフォームを作っていきます。
.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アクションを以下のようにします。
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
  .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も載っけておきます。
.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で表示させる方法を投稿したいと思います。
最後までお読みいただきありがとうございました!

