【Rails7】ActiveStorageで複数枚画像を投稿・更新する(プレビュー表示も実装)

ActiveStorageで複数枚画像を投稿(保存)する方法についての情報はたくさん出てきますが、正直どれも僕が思い描いていた画像アップロード機能のイメージとは違うかなぁと感じております。

例えば、僕が思い描いている複数枚画像アップロード機能のイメージは以下の通り。

  • 「ファイル選択」ボタンで追加したファイルのプレビューが表示される
  • 「ファイル選択」ボタンでファイルを追加する度にプレビュー表示に追加されていく
  • プレビュー表示から任意のファイルを削除できる
  • バリデーションエラー時でも選択したファイルが保持される
  • 編集時も上記と同様の動作を行う

こんな感じの画像アップローダーをActiveStorage + JavaScriptで実装したい…

そこで試行錯誤の末、なんとかイメージ通りの複数枚画像アップロード機能を実装することができました。

ゆえに今回は、ActiveStorage + JavaScript(Stimulus)を用いて実装した複数枚画像アップロード機能について、その実装の流れを覚書としてまとめました。

なお、単数枚の画像アップロード(プロフィール画像の登録、更新)の実装手順については以下の記事をご覧ください。

あわせて読みたい
【Rails】ActiveStorage でプロフィール画像(アバター)を登録・更新する ユーザーのプロフィール画像(アバター)を作成するにあたり、 ActiveStorageでファイルから画像をアップロード プロフィール編集画面にてアップロードした画像のプレビ...
目次

開発環境

  • Ruby 3.1.2
  • Ruby on Rails 7.0.4
  • tailwindcss 3.1.8
  • M1 Macbook Air 2020
  • mac OS Monterey (ver. 12.4)
  • ターミナル bash (Rosetta 2 使用

出来上がりイメージ

イメージとしては、一般的なWebアプリケーションでよく見かけるようなアップロード方法かと思います。

まず、「ファイル選択」ボタンで追加したファイルをプレビュー表示させます。

(「ファイル選択」ボタンで複数回に分けてファイルを追加した場合も、累積的にプレビューが表示されます)

今回の実装では、file_fieldにファイルをアタッチして送信する方法ではなく、「ファイル選択」ボタンでファイルを選択すると同時にDBにアップロードさせる方法(Ajax)を取りました。

というのも、file_fieldでは「ファイル選択」ボタンを押すたびに前回選択したファイルの内容が上書きされてしまいますし、バリデーションエラー時もfile_fieldの値がリセットされてしまうからです。(少なくともユーザーフレンドリーな仕様とは言えませんね)

また、以下のように「削除」ボタンで任意のファイルをプレビューから削除できるようにもしました。

(プレビューから削除されたファイルは投稿に反映されませんが、DB内には残り続けます。対処法は当記事後半にて)

また、編集時はDBに保存されている画像ファイルをプレビューで表示状態にし、あとは新規投稿時と同じようにファイルの追加、削除ができるようにしています。

バリデーションエラー時は選択状態のファイル(のプレビュー)を保持できるようにしています。

編集時のバリデーションエラーでも同様に、選択状態のファイルを保持できるようにしています↓

以上、こんな感じの画像アップローダーを実装していきます。

前提条件

  • Ruby on Rails 7.0.4 で実装
  • JavaScriptフレームワークはStimulusを使用
  • TailwindCSS導入済み
  • Postモデル作成済み

今回の実装において、JavaScriptフレームワークはRails7.0.4に標準搭載されているStimulusを使用しています。

Stimulusについて詳しく知りたい方は公式マニュアルを参考にしてみてください。

一応、過去記事にStimulusを用いて実装した機能もあるので、こちらも参考にしていただければと。

あわせて読みたい
【Rails7】JavaScript + Stimulusで動的なバリデーションチェックを実装してみた 以前、JavaScriptで動的なバリデーションチェックを実装する旨の記事を書きましたが、JSのコードが冗長なのと変数宣言にvarしか使えない(Turboとの相性により)という...
あわせて読みたい
【Rails7】Ajax + Stimulusでメールアドレスが登録済みかどうかチェックする方法 (jQueryなしで実装) Railsではフォーム入力時(新規ユーザー登録時)にすでに登録済みのメールアドレスがある場合、バリデーションが発動して登録できないようになっているかと思います。 ...

また、CSSはTailwindCSSを使用しています。

あわせて読みたい
【Rails】Tailwind CSS でカスタムCSS を作成・インポートする方法 最近、RailsでTailswindCSSを導入してみたのですが、思った以上に使い勝手が良い。 特に、カスタムコンポーネントでデザインを簡単に、自由自在に表現できる点は素晴ら...

複数枚画像のアップロード実装の流れ

それでは、ActiveStorageによる複数枚画像アップロード(Ajaxで非同期アップロード)の実装を進めていきます。

ActiveStorageを追加する

以下のコマンドでActiveStorageをインストールします。

$ rails active_storage:install
$ rails db:migrate

マイグレーションを実行すると以下3つのテーブルが作成されます。

スクロールできます
テーブル名内容
active_storage_blobsアップロードしたファイル(画像)を保存するテーブル(blob型)
active_storage_attachmentsアップロードしたファイルとActive Recordを紐付けるための中間テーブル
active_storage_variant_recordsアップロードしたファイルのVariantに関する情報を保存するテーブル

【blobとは】
DBのデータ型の一つで、画像や音声、圧縮ファイルなどのデータを保存するためのもの。

【variantとは】
Railsのblob型のデータに対して使えるメソッドの一つで、画像を呼び出したときに、ビューの中で画像サイズを変換することができる。

モデルの編集

作成済みのPostモデルを以下のように編集します。

class Post < ApplicationRecord
  has_many_attached :images # ←複数枚画像の場合。1枚のみの場合は has_one_attached :image

  validates :title, presence: true
end

ビュー(View)の作成

画像を投稿・編集、および表示するためのビューを作成します。

  • 新規投稿、編集用ビュー(_form.html.erbなど)
  • 表示用ビュー(index.html.erbなど)

まずは新規投稿、編集用のビュー_form.html.erbのサンプルから。

<%= form_with(model: @post, local: true, data: { controller: "images"}) do |f| %>

  <%= render 'layouts/errors', obj: @post %>

  <div class="mb-3">
    <%= f.label :title, "タイトル", class: "font-semibold block p-1" %>
    <%= f.text_field :title, autofocus: true, class: "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" %>
  </div>

  <div class="mb-3">
    <%= f.label :images, "画像", class: "font-semibold block p-1" %>
    <%= f.file_field :images, multiple: true, accept: "image/jpeg,image/gif,image/png", data: { images_target: "select", action: "change->images#selectImages" } %>
  </div>
  <div data-images-target="preview">
    <% if @post.images.attached? %>
      <% @post.images.each do |image| %>
        <div class="image-box inline-flex mx-1 mb-5" data-controller="images" data-images-target= "image_box">
          <div class="text-center">
            <%= image_tag image, width: "100" %>
            <a class="link cursor-pointer" data-action="click->images#deleteImage">削除</a>
            <%= f.hidden_field :images, name: "post[images][]", style: "display: none", value: "#{image.blob.id}" %>
          </div>
        </div>
      <% end %>
    <% end %>
  </div>

  <div class="mb-3">
    <%= f.submit  "投稿する", class: "btn btn-sky" %>
  </div>
<% end %>

今回、_form.html.erbにはJSフレームワークのStimulusを用いるため、要素中に以下のdata属性を設定しています。

  • data-controller
  • data-{controller}-target
  • data-action

data属性の設定方法はこちらの記事を参考にしてみてください。

続いては表示用のビューindex.html.erbのサンプルです。

<h1 class="text-3xl mb-3">投稿一覧</h1>
<div class="mb-3">
  <%= link_to '新規投稿', new_post_path, class: "btn btn-sky" %>
</div>
<%= render @posts %> // パーシャルを表示(以下参照)
<div class="my-8">
  <div id="post-<%= post.id %>" class="mb-3">
    <p class="font-bold">タイトル:<%= post.title %></p>
    <% if post.images.attached? %>
      <% post.images.each do |image| %>
        <div class="mb-2 inline-flex">
          <%= image_tag(image, width:100, class: "") %>
        </div>
      <% end %>
    <% end %>
  </div>
  <%= link_to "編集", edit_post_path(post), class: "btn btn-outline-gray" %>
  <%= link_to "削除", post, data: { turbo_method: :delete ,turbo_confirm: "削除してよろしいですか?" }, class: "btn btn-outline-gray" %>
</div>

コントローラーの編集

postsコントローラーを以下のように記述します。

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def index
    @posts = Post.all
  end

  def show
  end

  def new
    @post = Post.new
  end

  def edit
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to posts_path, notice: "投稿しました"
    else
      render "new", status: :unprocessable_entity
    end
  end

  def update
    if @post.update(post_params)
      redirect_to posts_path, notice: "更新しました"
    else
      render "edit", status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    redirect_to posts_path, notice: "削除しました", status: :see_other
  end

  # 画像アップロード用のアクション
  def upload_image
    @image_blob = create_blob(params[:image])
    render json: @image_blob
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end
  
  # 選択状態の画像をパラメータにマージ(Postモデルとの紐付け)
  def post_params
    params.require(:post).permit(:title).merge(images: uploaded_images)
  end

  # アップロード済み画像の検索
  def uploaded_images
    params[:post][:images].drop(1).map{|id| ActiveStorage::Blob.find(id)} if params[:post][:images]
  end

  # blobデータの作成
  def create_blob(file)
    ActiveStorage::Blob.create_and_upload!(
      io: file.open,
      filename: file.original_filename,
      content_type: file.content_type
    )
  end

end

ここでは、file_fieldで選択した画像をアップロードするためのアクションupload_imageを追加しています。

アップロードは、file_fieldで選択した画像をJS側で読み込んでupload_imageアクションに送信し、コントローラー側で受け取った画像ファイルからblobデータを生成しDB上に保存する、という流れで行います。

Postの投稿時には、merge(images: upload_images)で選択状態の画像をパラメータにマージすることでPostモデルとの紐付けを行います。

こうすることで、バリデーションエラー時も選択した画像が保持されたままになります。

ルーティングの設定

画像アップロードのアクション用のルーティング設定を行います。

post "posts/upload_image", to: "posts#upload_image"

Stimulusのコントローラー追加

以下のコマンドを実行してStimulusのコントローラーを追加します。

$ rails g stimulus コントローラー名
$ rails g stimulus images

Stimulusのコントローラーはapp/javascript/controllersディレクトリ以下に自動生成されます。

Stimulusのコントローラー編集

自動生成されたapp/javascript/controllers/images_controller.jsを開き、以下のように編集します。

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="images"
export default class extends Controller {
  /* ①静的プロパティを定義(data-{controller}-target で指定したターゲット名) */
  static targets = ["select", "preview", "image_box"]

  /* ②画像選択時の処理 */
  selectImages(){
    const files = this.selectTargets[0].files // file_fieldで取得した画像ファイル
    for(const file of files){
      this.uploadImage(file) // 選択した画像ファイルのアップロード
    }
    this.selectTarget.value = "" // 選択ファイルのリセット
  }

  /* ③画像アップロード */
  uploadImage(file){
    const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
    const formData = new FormData()
    formData.append("image", file) // formDataオブジェクトに画像ファイルをセット
    const options = {
      method: 'POST',
      headers: {
        'X-CSRF-Token': csrfToken
      },
      body: formData
    }
   /* fetchで画像ファイルをPostコントローラー(upload_imageアクション)に送信 */
    fetch("/posts/upload_image", options) 
      .then(response => response.json())
      .then(data => { // Postコントローラーからのレスポンス(blobデータ)
        this.previewImage(file, data.id) // 画像プレビューアクションにblobデータのidを受け渡す
      })
      .catch((error) => {
        console.error(error)
      })
  }

  /* ④画像プレビュー */
  previewImage(file, id){
    const preview = this.previewTarget // プレビュー表示用の<div>要素
    const fileReader = new FileReader()
    const setAttr = (element, obj)=>{ // 属性設定用の関数
      Object.keys(obj).forEach((key)=>{
        element.setAttribute(key, obj[key])
      })
    }
    fileReader.readAsDataURL(file) // ファイルをData URIとして読み込む
    fileReader.onload = (function () { // ファイル読み込み時の処理
      const img = new Image()
      const imgBox = document.createElement("div")
      const imgInnerBox = document.createElement("div")
      const deleteBtn = document.createElement("a")
      const hiddenField = document.createElement("input")
      const imgBoxAttr = { // imgBoxに設定する属性
        "class" : "image-box inline-flex mx-1 mb-5",
        "data-controller" : "images",
        "data-images-target" : "image_box",
      }
      const imgInnerBoxAttr = { // imgInnerBoxに設定する属性
        "class" : "text-center"
      }
      const deleteBtnAttr = { // deleteBtnに設定する属性
        "class" : "link cursor-pointer",
        "data-action" : "click->images#deleteImage"
      }
      const hiddenFieldAttr = { // hiddenFieldに設定する属性
        "name" : "post[images][]",
        "style" : "none",
        "type" : "hidden",
        "value" : id, // 受け取ったidをセット
      }
      setAttr(imgBox, imgBoxAttr)
      setAttr(imgInnerBox, imgInnerBoxAttr)
      setAttr(deleteBtn, deleteBtnAttr)
      setAttr(hiddenField, hiddenFieldAttr)

      deleteBtn.textContent = "削除"

      imgBox.appendChild(imgInnerBox)
      imgInnerBox.appendChild(img)
      imgInnerBox.appendChild(deleteBtn)
      imgInnerBox.appendChild(hiddenField)
      img.src = this.result
      img.width = 100;

      preview.appendChild(imgBox) // プレビュー表示用の<div>要素の中にimgBox(プレビュー画像の要素)を入れる
    })
  }

  /* ⑤プレビュー画像の削除 */
  deleteImage(){
    this.image_boxTarget.remove()
  }

}

長いので番号分けして順番に説明していきます。

①静的プロパティを定義

  /* ①静的プロパティを定義(data-{controller}-target で指定したターゲット名) */
  static targets = ["select", "preview", "image_box"]

まず最初に、HTMLのdata-{controller}-targetで指定したターゲット名をstatic targetsに定義しておきます。

こうすることで、例えばdata-images-target="select"と指定した要素を取得したい場合、JS側に以下のコードを記述することで要素を取得することができます。

this.selectTarget
// <input multiple="multiple" accept="image/jpeg,image/gif,image/png" data-images-target="select" data-action="change->images#selectImages" type="file" name="post[images][]" id="post_images">

また、以下のように複数形にした場合は要素を配列として取得することができます。

this.selectTargets
// ▼ [input#post_images]
//   ▶︎ 0: input#post_images (以下略)
//     length: 1
//   ▶︎ [[Prototype]]: Array(0) (以下略)

②画像選択時の処理

  /* ②画像選択時の処理 */
  selectImages(){
    const files = this.selectTargets[0].files // file_fieldで取得した画像ファイル
    for(const file of files){
      this.uploadImage(file) // 選択した画像ファイルのアップロード
    }
    this.selectTarget.value = "" // 選択ファイルのリセット
  }

file_fieldの「ファイル選択」ボタンでファイル選択時に行う処理を記述しています。

ここでは、this.selectTargets[0].filesで取得した画像ファイル(単数、もしくは複数)を、画像アップロード用のアクションthis.uploadImage(file)に受け渡しています。

今回は画像を選択した時点でDBに画像がアップロードされるため、file_fieldの値はリセットしておきます。

(リセットしておかないとfile_fieldに残ったファイルがsubmit時に送信され、すでにアップロードした画像と重複して保存されてしまうため)

③画像アップロード

  /* ③画像アップロード */
  uploadImage(file){
    const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
    const formData = new FormData()
    formData.append("image", file) // formDataオブジェクトに画像ファイルをセット
    const options = {
      method: 'POST',
      headers: {
        'X-CSRF-Token': csrfToken
      },
      body: formData
    }
   /* fetchで画像ファイルをPostコントローラー(upload_imageアクション)に送信 */
    fetch("/posts/upload_image", options) 
      .then(response => response.json())
      .then(data => { // Postコントローラーからのレスポンス(blobデータ)
        this.previewImage(file, data.id) // 画像プレビューアクションにblobデータのidを受け渡す
      })
      .catch((error) => {
        console.error(error)
      })
  }

file_fieldで選択した画像をアップロードするためのアクションです。

引数fileで受け取った画像ファイルを、formDataオブジェクトにセットしてfetchメソッドでPostコントローラー(upload_imageアクション)に送信しています。

(RailsアプリケーションでAjaxによるPOST送信を行う場合は、送信時にcsrfTokenトークンを添付する必要があります)

Postコントローラー側では受け取った画像ファイルを元にblobデータ(@image_blob)を作成し、render json: @image_blobでJSONデータとして返しています。

・・・

  # 画像アップロード用のアクション
  def upload_image
    @image_blob = create_blob(params[:image])
    render json: @image_blob
  end

  private
 ・・・
  # blobデータの作成
  def create_blob(file)
    ActiveStorage::Blob.create_and_upload!(
      io: file.open,
      filename: file.original_filename,
      content_type: file.content_type
    )
  end

そして、fetchメソッドのレスポンスでblobデータ(@image_blob)を受け取り、blobデータのidを画像プレビュー用アクションthis.previewImage(file, data.id)に受け渡しています。

    fetch("/posts/upload_image", options)   
      .then(response => response.json())
      .then(data => { // Postコントローラーからのレスポンス(blobデータ)
        this.previewImage(file, data.id) // 画像プレビューアクション実行時にblobデータのidを受け渡す
      })

④画像プレビュー

  /* ④画像プレビュー */
  previewImage(file, id){
    const preview = this.previewTarget // プレビュー表示用の<div>要素
    const fileReader = new FileReader()
    const setAttr = (element, obj)=>{ // 属性設定用の関数
      Object.keys(obj).forEach((key)=>{
        element.setAttribute(key, obj[key])
      })
    }
    fileReader.readAsDataURL(file) // ファイルをData URIとして読み込む
    fileReader.onload = (function () { // ファイル読み込み時の処理
      const img = new Image()
      const imgBox = document.createElement("div")
      const imgInnerBox = document.createElement("div")
      const deleteBtn = document.createElement("a")
      const hiddenField = document.createElement("input")
      const imgBoxAttr = { // imgBoxに設定する属性
        "class" : "image-box inline-flex mx-1 mb-5",
        "data-controller" : "images",
        "data-images-target" : "image_box",
      }
      const imgInnerBoxAttr = { // imgInnerBoxに設定する属性
        "class" : "text-center"
      }
      const deleteBtnAttr = { // deleteBtnに設定する属性
        "class" : "link cursor-pointer",
        "data-action" : "click->images#deleteImage"
      }
      const hiddenFieldAttr = { // hiddenFieldに設定する属性
        "name" : "post[images][]",
        "style" : "none",
        "type" : "hidden",
        "value" : id, // 受け取ったidをセット
      }
      setAttr(imgBox, imgBoxAttr)
      setAttr(imgInnerBox, imgInnerBoxAttr)
      setAttr(deleteBtn, deleteBtnAttr)
      setAttr(hiddenField, hiddenFieldAttr)

      deleteBtn.textContent = "削除"

      imgBox.appendChild(imgInnerBox)
      imgInnerBox.appendChild(img)
      imgInnerBox.appendChild(deleteBtn)
      imgInnerBox.appendChild(hiddenField)
      img.src = this.result
      img.width = 100;

      preview.appendChild(imgBox) // プレビュー表示用の<div>要素の中にimgBox(プレビュー画像の要素)を入れる
    })
  }

file_fieldで選択した画像をプレビュー表示するためのアクションです。

ここでは、以下のような流れで処理を行っています。

  1. uploadImage()アクションで受け取ったfileとidをセット
  2. fileReaderオブジェクトを作成し、fileReader.readAsDataURL(file)で受け取ったファイルを読み込む
  3. ファイル読み込み時にHTML上に画像プレビュー表示用の要素を追加する
  4. 読み込みが完了したらプレビューとして表示される

なお、previewImage(file, id)で受け取ったblobデータのidは、_form.html.erb上に生成した隠しフィールドに埋め込み、フォーム送信時にパラメータ(params[:post][:images])に付与して送信しています。

こうすることで、Postコントローラーでモデルと画像データを紐付けすることができます。

(バリデーションエラー時には選択した画像を保持、再表示するようになる)

⑤プレビュー画像の削除

  /* ⑤プレビュー画像の削除 */
  deleteImage(){
    this.image_boxTarget.remove()
  }

こちらは任意のプレビュー画像を削除するアクションです。

今回はimageBox要素ごと(隠しフィールドのblobデータのid含む)削除することで対応しています。

以上、これで複数枚画像のアップロード機能の実装は一通り終わりました。

画像アップロード枚数、およびファイルサイズの上限を設定する(バリデーション)

このままだと無限に画像をアップロードできてしまうので、1投稿あたりにアップロードできる画像の枚数およびファイルサイズに制限を設けたいと思います。

まずは出来上がりイメージを見てみましょう。

こちらは画像アップロード枚数の上限(最大10枚)を設けた場合の動作です↓

画像アップロード枚数の上限(10枚まで)

そして、こちらはファイルサイズの上限(1枚あたり2MBまで)を設けた場合の動作です↓

ファイルサイズの上限(1枚あたり2MB)

記述するコードですが、まず_form.html.erbにエラーメッセージ表示用の要素を追加します↓

<%= form_with(model: @post, local: true, data: { controller: "images"}) do |f| %>

  ・・・

  <div class="mb-3">
    <%= f.label :images, "画像", class: "font-semibold block p-1" %>
    <%= f.file_field :images, multiple: true, accept: "image/jpeg,image/gif,image/png", data: { images_target: "select", action: "change->images#selectImages" } %>
    /******************** ↓追加 **************************/
    <p data-images-target="error" class="text-red-600"></p>
    /****************************************************/
  </div>
  <div data-images-target="preview">
    <% if @post.images.attached? %>
      <% @post.images.each do |image| %>
        <div class="image-box inline-flex mx-1 mb-5" data-controller="images" data-images-target= "image_box">
          <div class="text-center">
            <%= image_tag image, width: "100" %>
            <a class="link cursor-pointer" data-action="click->images#deleteImage">削除</a>
            <%= f.hidden_field :images, name: "post[images][]", style: "display: none", value: "#{image.blob.id}" %>
          </div>
        </div>
      <% end %>
    <% end %>
  </div>

  <div class="mb-3">
    <%= f.submit  "投稿する", class: "btn btn-sky" %>
  </div>
<% end %>

続いて、app/javascript/controllers/images_controller.jsを以下のように編集します。


import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="images"
export default class extends Controller {
  static targets = ["select", "preview", "image_box", "error"] // ← "error" を追加

 ・・・

  /********************** 追加↓ *************************/
  imageSizeOver(file){ // アップロードする画像ファイルサイズの上限(2MB)を超えたかどうか判定
    const fileSize = (file.size)/1000 // ファイルサイズ(KB)
    if(fileSize > 2000){
      return true // ファイルサイズが2MBを超えた場合はtrueを返す
    }else{
      return false
    }
  }
 /*******************************************************/

  /* 画像選択時の処理(以下のように編集↓) */
  selectImages(){
    this.errorTarget.textContent = ""
    const uploadedFilesCount = this.previewTarget.querySelectorAll(".image-box").length // すでにアップロードされた画像の枚数
    const files = this.selectTargets[0].files // 選択した画像の枚数(これからアップロードする画像)
    if(files.length + uploadedFilesCount > 10){
      this.errorTarget.textContent = "画像アップロード上限は最大10枚です。"
    }else{
      for(const file of files){
        if(this.imageSizeOver(file)){
          this.errorTarget.textContent = "ファイルサイズの上限(1枚あたり2MB)を超えている画像はアップロードできません。"
        }else{
          this.uploadImage(file) // ファイルのアップロード
        }
      }
    }
    this.selectTarget.value = "" // 選択ファイルのリセット
  }

 ・・・

これで完成です。

画像アップロード枚数、およびファイルサイズの上限はお好きなように変更してください。

【追記】複数枚画像をドラッグ&ドロップで表示、アップロードする方法

今回実装したコードを元に、画像をドラッグ&ドロップで表示およびアップロードする機能を追加しました。

詳しくは下記記事をご覧ください。

あわせて読みたい
【Rails7】複数枚画像をドラッグ&ドロップで表示、アップロードする方法(Stimulusで実装) 以前、Active Storageで複数枚画像のプレビュー表示、およびアップロードする方法についてまとめました。(Stimulusで実装) https://plog.kobacchi.com/rails7-actives...

DBにアップロード後、モデルに紐付いていないファイルを定期的に削除する方法(自動化対応)

今回実装した複数枚画像アップロード機能は、「ファイル選択」ボタンでファイルを選択した瞬間にDBにアップロードされる(かつプレビュー表示される)ようになっています。

ここでもし、投稿フォームで選択状態のファイル(プレビュー表示されているファイル)を削除した場合、削除されたファイルは投稿に反映されませんが、DB内には残り続けてしまいます。

DB内に残り続けているファイルはモデルと紐付いていないためビュー上に表示されることはありませんが、データベースのリソースを無駄に使うことになるため、このようなファイルは定期的にDB上から削除しておくのがベターです。

そこで、便利なのが以下のコードです。

ActiveStorage::Blob.unattached.find_each(&:purge)

このコードを実行すれば、モデルに紐ついていないファイルをまとめて削除することができます。

上記コードはRailsコンソール上から実行することができますが、手動で削除しなければならないのは少々面倒です。

そこで、Rails標準搭載のRakeタスク機能を駆使して、定期的にDB内の不要なファイルを清掃してくれるようにします(自動化)。

詳しいやり方は下記記事を参考にしてみてください↓

あわせて読みたい
【Rails7】Rakeタスクをwheneverで定期的に実行する方法(タスクの自動化) RailsにはRakeというgemが標準搭載されており、Railsで定期的に実行したい処理をRakeタスクとして定義しておくことで、必要なときに呼び出して実行することができます。...

参考資料

kykt35’s diary
ActiveStorageを使った複数画像アップロードアプリを作る - kykt35’s diary はじめに Active Storageを使って複数の画像を扱う方法についてまとめます。 Active Storageは、Rails 5.2で追加されたファイルアップロードの機能です。必要なテーブルも...
Qiita
Active Storageで複数画像の投稿・削除 - Qiita #環境Ruby 2.5.1Rails 5.2.4.3#やりたい事Rails標準のファイル管理機能Active Storageを使い、画像を複数を投稿・削除できる機能を実装したい。###まず…
Qiita
[Rails5.2]ActiveStorageの仕組み(図あり)と使ってみてわかったこと - Qiita 遅ればせながらRails5.2で導入されたActiveStorageを使ってみました。GitHubのReadmeやRailsGuidesの説明だけでなくソースコードも(一部)読んで調べたので、大ま…
あわせて読みたい
Rails コンソールから Active Storage のファイルアップロードを実行する - 暇人じゃない Active Storage が用意した Web API を使用せず、Rails のコンソールやバッチなどでファイルをアップロードしたいことがあったので、方法をメモする。 Rails のバージョン...
GRAYCODE
Fetch APIでフォームに入力されたデータを送信する | GRAYCODE JavaScript フォームに入力されたデータ(入力値)をFetch APIによるAjaxでサーバーへPOST送信する方法について解説します。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次