【Rails】gem refileで複数画像アップロードを実装する3つのステップ

# はじめに
　Rubyアプリケーションへファイルアップロード機能の実装を検討する際、手軽に利用できる主なgemにはRefile、CarrierWave、Shrine等があります。
　RefileとCarrierWaveは同じ作者によるもので、両者を利用して実装できる機能の中には共通しているものがあります。それらの内、Refileによる複数ファイルアップロード機能では、同gemでの単一アップロード機能の場合とは異なり**「1対Nのアソシエーション関係をもつモデル」が機能実装に必須の条件**[(*1)](#reference)である点に注意が必要です。
　また、ファイルを追加アップロードする際、デフォルトでは上書きアクションとなり、先に登録してあった元のファイルがすべて消えてしまうので「ファイルの上書きではなく追加をしたい場合」の設定記述[(*1)](#reference)がある点にも注意が必要です。

　本記事では gem "Refile"を利用し、Ruby on Railsで開発したCRUD WEBアプリケーションへの複数画像アップロード機能の実装手順を解説したいと思います。なお、開発環境の作成方法とgemの導入は割愛し、アプリのMVCを構成する各種ファイルの記述にフォーカスします。

　繰り返しになりますが、**1:Nのアソシエーション関係をもつモデルに渡されるファイルだけ**[(*1)](#reference)を処理する特徴があるので、実装を検討する際には設計をよく確認しましょう。

### アプリケーション概要<a id="#er"></a>
``下記解説では、モデル同士の1:Nのアソシエーション関係を簡易に表現するため、Articleモデルを親モデル、ArticleImageモデルを子モデルと呼んでいます。``
![MVC architecture](https://raw.githubusercontent.com/akahito1006/qiita/refileMulti/image1.jpg "概要図")

```rb:routes.rb
Rails.application.routes.draw do
  root to: 'articles#index'
  resources :articles, only: [:new, :create, :show, :edit, :update]
end
```

### 開発環境
- Ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
- Rails 5.2.6
- ImageMagick-7.0.11
- Bootstrap 5.0.0.beta1 css only (CDN powered by jsDelivr)
- *gem "Refile"
- *gem "Refile-mini_magick"
- Amazon Linux2(Karoo) by AWS Cloud9

# 実装ステップ
1. [投稿ビュー](#Chapter1)
    formヘルパー内、`attachment_field` の`multiple`属性をtrueにする

1. [コントローラ](#Chapter2)
    ストロングパラメータのオブジェクト名に親モデルを指定し、キーでは配列を許可する

1. [親・子モデル](#Chapter3)
    `accepts_attachments_for` マクロと `attachment`, `append`オプションを設定する


- [参考文献](#reference)
- [付録: 解説で使用したアプリケーションのGitHubリポジトリ](#git)

<a id="#Chapter1"></a>
# 1. 投稿ビュー
image_idカラムを持つ子モデルではなく、親モデルのformを使います。
`attachment_field`では子のテーブルの`image_id`カラムを指定します。

```erb:articles/new.html.erb
<%= form_with model: @article do |f| %> <!-- @articles = Article.all -->
  <%= f.text_field :title %>
  <%= f.attachment_field :article_images_images, multiple: true %>
  <%= f.submit "記事を作成する" %>
<% end %>
```
- 上記コードブロック3行目 `f.attachment_field`を編集
  -  キーの書き方：  
`:(子のテーブル名)_(image_id + s)` =>`:article_images_images` (2つの**"s"**が必要です)
  -  属性を追加：  
`~~~ , multiple: true`

このように設定すると、単一のファイルではなく**複数ファイルの配列**を受け取るようになります。（ファイルは1つでも可）
※その後の処理の流れでは、アップロードされたファイル毎に個別で種々の処理が実行されていきます[(*1)](#reference)。
　2つアップロードしたら2回uploadイベントが起き、SQL文も2回分発行されます。

<a id="#Chapter2"></a>
# 2. コントローラ
ストロングパラメータのオブジェクト名に親モデルを指定し、キーでは配列を許可します。
`permit`の中には親テーブルのカラム名が並んでいるかと思います。
その中に、ステップ1で書いたキーを転記し、`article_images_images: []`のように書きます。

```rb:articles_controller.rb
class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    @article.save
    redirect_to root_path
  end
# ---strong parameter below---
  private
  def article_params
    params.require(:article).permit(:title, article_images_images: [])
  end
end
```
- 上記コードブロック10行目 `params`メソッドを編集
  -  データのオブジェクト名：  
`require(:親モデル名)` =>`require(:article)` (モデル名なので**"s"**なし)
  -  キーを追加：  
`permit(:親テーブルのカラム名, ..., :(子のテーブル名)_(image_id + s): [])`  
  => `permit(:title, ..., article_images_images: [])` 

子モデルに対し、アップロードされたファイル毎にsave処理が繰り返される[(*1)](#reference)ので、
配列の各要素に格納されたファイルがひとつずつ、子のテーブルのimage_idカラムに保存されていきます。

**例）複数ファイルアップロードで2つの画像ファイルを保存した後の`article_images`テーブル**

| id | article_id | image_id | created_at | updated_at |
| :---: | :---: | :---: | :---: | :---: |
| 1 | 1 | e2a5ca... | 2021-05-19 18:03:20 | 2021-05-19 18:03:20 |
| 2 | 1 | fddd44... | 2021-05-19 18:03:20 | 2021-05-19 18:03:20 |


```sql:console
$ rails c
...
2.6.3 :001 > ArticleImage.all
  ArticleImage Load (1.8ms)  SELECT  "article_images".* FROM "article_images" LIMIT ?  [["LIMIT", 11]]
 => #<ActiveRecord::Relation [
      #<ArticleImage id: 1, article_id: 1, image_id: "e2a5ca72b5157b62198d21d81e3630133bb566b819f0718f40...", created_at: "2021-05-19 18:03:20", updated_at: "2021-05-19 18:03:20">,
      #<ArticleImage id: 2, article_id: 1, image_id: "fddd4435a01a5189eac73dcc4b78f555c73b138fc918bb7148...", created_at: "2021-05-19 18:03:20", updated_at: "2021-05-19 18:03:20">
    ]> 
2.6.3 :002 > ArticleImage.find(2)
  ArticleImage Load (0.2ms)  SELECT  "article_images".* FROM "article_images" WHERE "article_images"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
 => #<ArticleImage id: 2, article_id: 1, image_id: "fddd4435a01a5189eac73dcc4b78f555c73b138fc918bb7148...", created_at: "2021-05-19 18:03:20", updated_at: "2021-05-19 18:03:20"> 
```
　**例**で示す通り、単一アップロードの場合と同じように個別でレコードが出来るので、保存後に`find`メソッドで1つずつ呼び出すこともできます。
※なお、子のテーブルには`article_id`というカラムがありますが、テーブル同士のリレーション関係がある[(*ER図)](#er)ので、ファイル保存の際にレコードが生成されると自動で親のレコードの`id`が入力されます。


<a id="#Chapter3"></a>
# 3. 親・子モデル
親モデルに`accepts_attachments_for` マクロと `attachment`, `append`オプションを設定します。

```rb:親：article.rb
class Article < ApplicationRecord
  has_many :article_images, dependent: :destroy
  accepts_attachments_for :article_images, attachment: :image, append: true
end
```
- 上記コードブロック3行目
  -  マクロの設定：  
`accepts_attachments_for :子のテーブル名` =>`accepts_attachments_for :article_images`
  -  画像アップロード用のメソッドを追加：  
`attachment: :image`（**コロン":"** は2つ）
  -  オプション：  
update編集アクションの際、**ファイルの上書き（消す）**または**元のファイルを保持して追加（消さない）**を切り替える  
      -  ファイルを上書きする・・・デフォルト設定、`オプションを書かない`
      -  元のファイルを保持し追加アップロードする・・・`append: true`


また、子モデルにも `attachment`メソッドを追加します。書き方は単一アップロードの場合と同様です。

```rb:子：article_image.rb
class ArticleImage < ApplicationRecord
  belongs_to :article
  attachment :image
end
```
# おわりに
　Refileによる複数ファイルアップロード機能では**「1対Nのアソシエーション関係をもつモデル」が機能実装に必須の条件**[(*1)](#reference)であり、ファイルを追加アップロードする際に**「ファイルの上書きではなく追加をしたい場合」**の設定記述[(*1)](#reference)がある点に注意が必要です。
　RefileはCarrierWaveの後継ですが、本記事で紹介した機能のようにモデルのアソシエーションが必須である(*1)など設計上の制約があり、一方でより自由にカスタマイズできるCarrierWaveの方が人気が高いようです[(*2)](#reference)。
　Refileの前身であるCarrierWaveは公開から10年以上経った今でも盛んにアップデートされ、月間ダウンロード数やGitHubスター数で後継のRefileを圧倒する人気のgemの1つです[(*2)](#reference)。また、v2.0以後はRails5.0以上、Ruby2.2以上に対応しています。
　より自由な設計条件で複数ファイルアップロード機能の実装を検討する場合には、Refileだけでなく他のgemを検討することも重要だと思います。

<a id="#reference"></a>
### 参考文献
- [*1 GitHub refile/README.md "Multiple file uploads"](https://github.com/refile/refile/blob/master/README.md)
- [*2 Refile vs CarrierWave | LibHunt](https://ruby.libhunt.com/compare-refile-vs-carrierwave)

<a id="#git"></a>
### 付録
- 解説で使用したアプリケーションのGitHubリポジトリ(https://github.com/akahito1006/qiita/tree/refileMulti)