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

## 何をしたか
### 投稿に紐づく画像（複数）について思うこと
Railsの課題を実施しています。instagramのようなアプリを作っていて、条件は以下の通りです。

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

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

（実は、過去にこの実装はやったことがあって、[こんな記事](https://qiita.com/tanutanu/items/47f8a229ef52cae3c251)も書いていた。）

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

[ActiveRecord serialize / store の甘い誘惑を断ち切ろう](https://qiita.com/jnchito/items/68e91e9bf46f960a79e4)

### では、何をしたか

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

[![Image from Gyazo](https://i.gyazo.com/3ff5532b162755ce3983340414e8fc9a.png)](https://gyazo.com/3ff5532b162755ce3983340414e8fc9a)

ユーザーが投稿をもち、それに紐づく複数のイメージがあるという図になります:relaxed:
そして、`accept_nested_attributes_for`を使うと楽なんだろうなあ。。。。と思いつつ、こちらもあまりよくない実装と聞いていたので、復習も兼ねて`form_object`を使うことにしました。

↓参考までに、過去記事を紹介します。
▼`accept_nested_attributes_for`を使ったフォームは実装したことがあった
[fields_forで子テーブルのデータを一気に作成する（テストも書いてます）[Rails][Rspec]](https://qiita.com/tanutanu/items/c82c1ed2f5fa78a58366)

▼`form_object`も作ったことがあったけど、よくわかっていなかった
[form_objectで親子関係のあるフォームを作成する（テストも書いてます）](https://qiita.com/tanutanu/items/51b91688be6047b78bd8)

## 実装
それでは、早速実装していこうと思います。なお、実行環境は以下の通りです。

- `Rails 5.2.3`
- `Ruby 2.6.0`

### CarrierWaveで画像を投稿できるようにする
Carrierwaveの使い方については、非常に優れた多くの記事があるので多くは書きません。自分が見つけた限りではこちらの記事が一番詳しくてわかりやすいと思いますので、そちらをご覧ください。

[【Rails】CarrierWaveチュートリアル](https://pikawaka.com/rails/carrierwave)

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

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

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

```ruby: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のモデルファイルには以下の様に記載します。

```ruby:models/image.rb
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`が投稿されたときに画像も投稿できるようにしたいと思います。まずは１枚投稿できる様にします。

#### view
```ruby: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
```ruby: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

```Ruby: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を作成する](https://tech.mof-mof.co.jp/blog/rails-form-object/)

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

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

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

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

```ruby: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`に変えていますが、これは必須ではなく、わかりやすさのために変えています。（ただし、この変更はこの後紹介する`controller`と`form_object`のコードにも影響しています）

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

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

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

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

```Ruby: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!メソッドを回しました。

### 完成
上記で作成されたレコードが下記の通りです。きちんと別のファイルで投稿に紐づく画像が２つ作成されていますね: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機能を実装した話](https://qiita.com/tanutanu/items/5ff5f1e653d802929f0b)