大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Active Storage】単数画像→複数画像への移行方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

画像アップロード機能で少しだけ詰まったところがあるので、本記事ではそれを紹介します。

詰まった箇所は「Active Storageによる複数画像のアップロード方」です。画像アップロード方法として、自分は今までRUNTEQの基礎編課題でCarriWaveを学習していました。調べてみると、Active StorageはRails5.2よりgemではなく公式にファイルアップロード機能として追加されたようです。

今回使用するアプリはいわゆる「パルビ」と呼ばれる書籍のActive Stoageの項目を実装した状態から始めます!

パーフェクト Ruby on Rails 【増補改訂版】 (Perfect series)

現在のファイルの状況

アプリケーションはscaffoldを使用して作成されています。ファイルアップロードの機能がある箇所はユーザー作成ページです。

https://i.gyazo.com/8e35e80d762668e51e116d56e7454d4a.png

こちらに画像をアップしてユーザーを作成すると、ユーザー詳細ページにて画像が表示されます。

https://i.gyazo.com/3f40d2b691abdaa3b60dd08616a1d25b.png

詳しくコードを見ていきます。

まずUserモデルにはhas_one_attached :portraitとアソシエーションが指定されています。

# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :portrait
end

この:portraitは実際にはUserモデルのカラムにはありません。これはActive Storageがポリモーフィックなアソシエーションを採用しているからです。ポリモーフィックなアソシエーションは説明が大変複雑なので今後別の記事で説明する予定です。現状は下記記事を自分は参考にしているので、気になる方は下記を参照してください。

Railsのポリモーフィック関連とはなんなのか - Qiita

ここでの理解は、「ユーザーとは別のモデルであるActiveStorage::Blobが:portraitを持っており、ユーザーは:portraitと関連づけられている」という理解で大丈夫かなと思います。

ユーザー作成画面の名前入力とファイルアップロード箇所のerbの現状です。

<!-- app/views/users/_form.html.erb -->
<div class="field">
  <%= form.label :name %>
  <%= form.text_field :name %>
</div>

<div class="field">
  <%= form.label :portrait %>
  <%= form.file_field :portrait %>
</div>

こちらで入力されたnameとアップロードしたportraitがパラメーターとして渡されます。user_paramsでストロングパラメーターを介して値を取得しています。ユーザー作成であるcreateアクションでそれらの値を受け取って保存できればユーザー詳細ページをレンダリングします。

# app/controllers/users_controller.rb

def create
  @user = User.new(user_params)

  respond_to do |format|
    if @user.save
      format.html { redirect_to @user, notice: "User was successfully created." }
      format.json { render :show, status: :created, location: @user }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @user.errors, status: :unprocessable_entity }
    end
  end
end
・
・
・
def user_params
  params.require(:user).permit(:name, :portrait)
end

ユーザー詳細ページです。image_tagで画像を表示していることがわかります。

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @user.name %>
</p>

<p>
  <strong>Portrait:</strong>
  <%= image_tag @user.portrait.variant(resize_to_limit: [100, 100]) %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

variant(resize_to_limit: [100, 100])のメソッドはデフォルトでgemにコメントアウトされた状態で記載されているimage-processingをインストールすることで使用できます。これにより画像のサイズを指定することができます。

# Gemfile
gem 'image_processing', '~> 1.2'

以上ががざっくりとした現状のファイルの状態です。Active Storageのファイルアップロード機能は比較的簡単に実装することができます。しかし、現在のファイル状態では画像を一つしかアップロードすることができません。そこで、現在のファイル状態を変更し、複数の画像をアップロードできるように実装したいと思います。

複数の画像をアップロードできるように変更

モデルの編集

まず、複数のファイルを格納することができるように、モデルを変更します。Active Storageではファイルをそのモデルが所有する関係性をかなり直感的に表しています。has_one_attached :portraitとあるように、Userモデルがportrait(1つの画像)をアタッチメントとして所有していることがわかります。

では、複数の画像をアップロードする場合は?

実は、has_many_attached :portraitsと変更をするだけです。

# app/models/user.rb
class User < ApplicationRecord
  # has_one_attached :portrait
  has_many_attached :portraits
end

これにより、「Userモデルがportraits(複数の画像)をアタッチメントとして所有している」ことを示しています。

アップロード画面を編集

次に、パラメーターを渡すビューを変更します。

これも直感的で、portraitをportraitsに変更します。

<div class="field">
  <%= form.label :portrait %>
  <!-- <%= form.file_field :portrait %> -->
  <%= form.file_field :portraits %>
</div>

ただ、これだけでは複数のファイルをパラメーターとして渡すことはできるようにしたけど、複数のファイルをアップできるようにHTMLが対応されていません。対応するためにはオプションにmultiple: trueを追記します。

<div class="field">
  <!--  <%= form.label :portrait %> -->
  <%= form.label :portraits %>
  <!-- <%= form.file_field :portrait %> -->
  <%= form.file_field :portraits, multiple: true %>
</div>

ストロングパラメーターを編集

次はストロングパラメーターです。

もうどこを変更すればいいかわかると思います。「portraitをportraitsに変更」ですね。

def user_params
    # params.require(:user).permit(:name, :portrait)
  params.require(:user).permit(:name, :portraits)
end

と私も最初は思ったのですが、実は違います!!!!

正しい実装は以下のとおりです。

def user_params
    # params.require(:user).permit(:name, :portrait)
    # params.require(:user).permit(:name, :portraits)
    params.require(:user).permit(:name, portraits: [])
end

これはアップロードされるファイルが複数であることに対応するためです。空の配列を指定しないと複数のファイルを格納する器がないということです。

詳細ページを編集

最後にユーザー詳細ページです。現状image_tagは1つしかないので複数の画像を表示できません。複数画像を表示するために、アップロードされたportraitsの数だけimage_tagが生成されるように繰り返し処理をします。

<p>
  <strong>Portrait:</strong>
  <% @user.portraits.each do |portrait| %>
    <%= image_tag portrait.variant(resize_to_limit: [100, 100]) %>
  <% end %>
</p>

これで複数の画像が表示するように実装することができました!!

https://i.gyazo.com/53ab15b583c1ac5ff7afff3ae70aac6f.gif

終わりに

単数から複数の画像をアップロードすること自体は難しくありません!単数形を複数形に変更するみたいな作業ですからね。

ただ、大きなアプリケーションともなると、様々なファイルで画像のカラムが使われていたら大変です。例えば、画像のバリデーションをするデコレイターがあったとすると、それが単数ようのファイルで定義されていた場合は、複数のファイルを持つ配列ではエラーが起きてしまいます。なのでアプリケーションを作成する際は、なるべく単数か複数かを明確な理由を持って決めておくと、後々変更するという手間を減らすことができるでしょう。

以上、大ちゃんの駆け出し技術ブログでした!