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

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

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

#目指すゴール
目指すのはこ〜んな感じです！
![ezgif.com-video-to-gif (9).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/b6e0730d-3b3e-5dbd-5a65-66ce35ac805f.gif)
#前提
1. 画像のアップロードには`gem`の`CarrierWave`と`MiniMagick`を使用。
2. 画像は複数枚アップロードできる。
3. テーブル同士の関係は以下の通り。(実際のアプリからは簡略化しています)
<img width="534" alt="スクリーンショット 2020-06-14 18.16.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653268/44da731f-9d46-d883-3623-8128cfbef7fd.png">

##開発環境
ruby 2.5.1
Rails 5.2.4.2
Haml 5.1.2

#画像を複数投稿する方法
まず、どのような方法があるか調べました。3つありました。（他にもあるかもです）
1. `gem`の`CarrierWave`が用意している`"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を使わず、複数の子レコードを保存する](https://moneyforward.com/engineers_blog/2018/12/15/formobject/)
画像を1枚保存するところまではできましたが複数は実装できず、期日も迫っていたので、3の方法をとることにしました。

Railsの生みの親が"kill"したいらしい`accepts_nested_attributes_for`メソッドですが、`Railsガイド`にバッチリ載ってます！
【参考】[公式ドキュメント:Active Record Nested Attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html)（[日本語版はこちら](https://railsguides.jp/action_controller_overview.html#%E3%83%8D%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF)）
これも正攻法だ！！と思い直し、このメソッドを使い実装していきますっ

#いざ実装

`CarrierWave`の導入はこちらをご参考ください▶︎[【Rails入門】CarrierWaveを使って画像のアップロードに挑戦！](https://www.sejuku.net/blog/65175)
他にも調べればいっぱいサイトが出てきます。

##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. ビューに関するコントローラの記述をしていきましょう
###ルーティング
グループチャットなので、投稿機能である`posts`を`groups`にネストしたルーティングになります。

```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
```
###コントローラ
`post`は`group`に紐づいているので、`before_action`で`group_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
```

ポイントはこの部分！

```haml
= 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
```
ポイントはストロングパラメータ

```ruby
  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;
    }
  }
}
```
ハマったのはもちろんここ。

```haml
- if post.post_files.present?
  .post-files
    - post.post_files.each do |file|
      %object{data: file.file.url, class: 'post-files__file'}  <---ここ!!
```
最初にどう記述していたかといいますと、以下です。

```haml
      %object{data: file.url, class: 'post-files__file'}
```
この記述だと、
`NoMethodError` `undefined method 'url' for #<PostFile:0x00007fd979263950>`
ってなります。
原因は、`each`で一つずつ取り出した`post_files`はカラム名までしっかり指定してあげる必要があるからです。

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

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

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

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

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

#参考サイト
- [公式ドキュメント:Active Record Nested Attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html)
- [Railsのaccepts_nested_attributes_forについて解説してみた。](https://hirocorpblog.com/post-213/)
- [画像の複数枚投稿と編集とプレビューと私](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)