はじめに
Rubyアプリケーションへファイルアップロード機能の実装を検討する際、手軽に利用できる主なgemにはRefile、CarrierWave、Shrine等があります。
RefileとCarrierWaveは同じ作者によるもので、両者を利用して実装できる機能の中には共通しているものがあります。それらの内、Refileによる複数ファイルアップロード機能では、同gemでの単一アップロード機能の場合とは異なり**「1対Nのアソシエーション関係をもつモデル」が機能実装に必須の条件**(*1)である点に注意が必要です。
また、ファイルを追加アップロードする際、デフォルトでは上書きアクションとなり、先に登録してあった元のファイルがすべて消えてしまうので「ファイルの上書きではなく追加をしたい場合」の設定記述(*1)がある点にも注意が必要です。
本記事では gem "Refile"を利用し、Ruby on Railsで開発したCRUD WEBアプリケーションへの複数画像アップロード機能の実装手順を解説したいと思います。なお、開発環境の作成方法とgemの導入は割愛し、アプリのMVCを構成する各種ファイルの記述にフォーカスします。
繰り返しになりますが、1:Nのアソシエーション関係をもつモデルに渡されるファイルだけ(*1)を処理する特徴があるので、実装を検討する際には設計をよく確認しましょう。
アプリケーション概要
下記解説では、モデル同士の1:Nのアソシエーション関係を簡易に表現するため、Articleモデルを親モデル、ArticleImageモデルを子モデルと呼んでいます。
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
実装ステップ
-
投稿ビュー
formヘルパー内、attachment_field
のmultiple
属性をtrueにする -
コントローラ
ストロングパラメータのオブジェクト名に親モデルを指定し、キーでは配列を許可する -
親・子モデル
accepts_attachments_for
マクロとattachment
,append
オプションを設定する
1. 投稿ビュー
image_idカラムを持つ子モデルではなく、親モデルのformを使います。
attachment_field
では子のテーブルのimage_id
カラムを指定します。
<%= 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)。
2つアップロードしたら2回uploadイベントが起き、SQL文も2回分発行されます。
2. コントローラ
ストロングパラメータのオブジェクト名に親モデルを指定し、キーでは配列を許可します。
permit
の中には親テーブルのカラム名が並んでいるかと思います。
その中に、ステップ1で書いたキーを転記し、article_images_images: []
のように書きます。
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)ので、
配列の各要素に格納されたファイルがひとつずつ、子のテーブルの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 |
$ 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図)ので、ファイル保存の際にレコードが生成されると自動で親のレコードのid
が入力されます。
3. 親・子モデル
親モデルにaccepts_attachments_for
マクロと attachment
, append
オプションを設定します。
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
メソッドを追加します。書き方は単一アップロードの場合と同様です。
class ArticleImage < ApplicationRecord
belongs_to :article
attachment :image
end
おわりに
Refileによる複数ファイルアップロード機能では**「1対Nのアソシエーション関係をもつモデル」が機能実装に必須の条件**(*1)であり、ファイルを追加アップロードする際に**「ファイルの上書きではなく追加をしたい場合」**の設定記述(*1)がある点に注意が必要です。
RefileはCarrierWaveの後継ですが、本記事で紹介した機能のようにモデルのアソシエーションが必須である(*1)など設計上の制約があり、一方でより自由にカスタマイズできるCarrierWaveの方が人気が高いようです(*2)。
Refileの前身であるCarrierWaveは公開から10年以上経った今でも盛んにアップデートされ、月間ダウンロード数やGitHubスター数で後継のRefileを圧倒する人気のgemの1つです(*2)。また、v2.0以後はRails5.0以上、Ruby2.2以上に対応しています。
より自由な設計条件で複数ファイルアップロード機能の実装を検討する場合には、Refileだけでなく他のgemを検討することも重要だと思います。
参考文献
付録
- 解説で使用したアプリケーションのGitHubリポジトリ(https://github.com/akahito1006/qiita/tree/refileMulti)