【Rails】ActiveStorage でプロフィール画像(アバター)を登録・更新する

ユーザーのプロフィール画像(アバター)を作成するにあたり、

  1. ActiveStorageでファイルから画像をアップロード
  2. プロフィール編集画面にてアップロードした画像のプレビュー
  3. variantによる画像のリサイズおよび切り抜き(画像登録時)
  4. アップロードした画像の削除

これらの処理を行う方法についてまとめてみました。

複数枚の画像アップロード(プレビュー機能あり)の実装手順については以下の記事をご覧ください。

あわせて読みたい
【Rails7】ActiveStorageで複数枚画像を投稿・更新する(プレビュー表示も実装) ActiveStorageで複数枚画像を投稿(保存)する方法についての情報はたくさん出てきますが、正直どれも僕が思い描いていた画像アップロード機能のイメージとは違うかなぁ...
目次

開発環境

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

出来上がりのイメージ

完成イメージは以下の通りです。

プロフィール編集画面でプロフィール画像を選択するとプレビューが表示され、更新するとviewページ(プロフィールページ)に登録した画像が表示されます。

(variantメソッドで指定したサイズにリサイズし、正方形に切り抜きを行う画像処理を施しています)

プロフィール画像を変更する際は、再度プロフィール編集画面にて別の画像を選択→更新すると新しいプロフィール画像が登録されます(古いプロフィール画像は自動的に削除される)。

また、登録されている画像を編集時に削除することもできるようにしています。

以上のような完成イメージを目指して進めていきます。

ActiveStorageでアバター画像をアップロードする流れ

【前提】
・Userモデル作成済み
・Gravatar導入済み(アバター画像が登録されていない場合にGravatarを使用)
・Bootstrap5 導入済み

上記を前提とした上で実装を進めていきます。

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型のデータに対して使えるメソッドの一つで、画像を呼び出したときに、ビューの中で画像サイズを変換することができる。

ただし、表示するビューのidをinteger型ではなくstring型(以下のようなランダムな文字列)にしている場合、このままだとactive_storage_attachmentsテーブルで画像との紐付けがうまくいきません。

例)http://localhost:3000/user/show/GwUzoWxCL9bZ8fdB(←文字列のid)

あわせて読みたい
【Rails】各種idをランダムな文字列に変換してPrimary keyとして使う Railsでデータベースに値を登録すると、デフォルトでは各テーブルのid(integer型)が1から順番に割り当てられるようになっています。 しかし、このままだと(以下の例...

表示するビューのidがstring型の場合においては、ActiveStorageをインストールしてマイグレーションを実行する前にactive_storage_attachmentsテーブルのrecord_idをstring型に変更しておく必要があります。

(デフォルトでは、record_idの型はbigintとなる)

したがって、

$ rails active_storage:install

を実行時に自動生成されたマイグレーションファイルに、以下のようにtype: :stringを追記しておきます。

class AddActiveStorageTable < ActiveRecord::Migration[7.0]
  def change
    # Use Active Record's configured type for primary and foreign keys
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table :active_storage_blobs, id: primary_key_type do |t|
      t.string   :key,          null: false
      t.string   :filename,     null: false
      t.string   :content_type
      t.text     :metadata
      t.string   :service_name, null: false
      t.bigint   :byte_size,    null: false
      t.string   :checksum

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :key ], unique: true
    end
    
    # user_id をランダムな文字列(string)にしている場合は, :record カラムのタイプをstringにしておく
    create_table :active_storage_attachments, id: primary_key_type do |t|
      t.string     :name,     null: false
      #---------------------- type: :string を追記 -------------------------------#
      t.references :record,   null: false, polymorphic: true, index: false, 
                                           type: foreign_key_type, type: :string
      #--------------------------------------------------------------------------#
      t.references :blob,     null: false, type: foreign_key_type

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end

    create_table :active_storage_variant_records, id: primary_key_type do |t|
      t.belongs_to :blob, null: false, index: false, type: foreign_key_type
      t.string :variation_digest, null: false

      t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end

  private
    def primary_and_foreign_key_types
      config = Rails.configuration.generators
      setting = config.options[config.orm][:primary_key_type]
      primary_key_type = setting || :primary_key
      foreign_key_type = setting || :bigint
      [primary_key_type, foreign_key_type]
    end
end

追記し終えたら以下のコマンドでマイグレーションを実行してテーブルを作成します。

$ rails db:migrate

ActiveStorageバリデーション用のgemを追加する

ActiveStorage単体にはバリデーション機能がサポートされていないため、以下のgemを追加します。

gem "active_storage_validations"

以下のコマンドを実行してインストールします。

$ bundle install

Userモデルに記述

Userモデルに以下のように追記します。

class User < ApplicationRecord
  has_one_attached :avatar
  ・・・
  validates :avatar, content_type: { in: %w[image/jpeg image/gif image/png],
                    message: "有効なフォーマットではありません" },
                    size: { less_than: 5.megabytes, message: " 5MBを超える画像はアップロードできません" }
  ・・・
end

画像を1枚だけアップロードする場合はhas_one_attached: :〇〇、複数枚アップロードする場合はhas_many_attached: :〇〇を追記します。

バリデーションでは有効なフォーマットと、アップロードサイズの上限を指定しておきます。

controllerに記述

コントローラー(今回はusers_controller.rb)に以下を追記します。

class UsersController < ApplicationController
 ・・・
  def create
    @user = User.new(user_params)
    #----------------- 追記 -------------------
    @user.avatar.attach(params[:user][:avatar])
    #-----------------------------------------
    if @user.save
      @user.send_activation_email
      flash[:notice] = "アカウント認証メールを送信しました。メールが届きましたら、24時間以内に本文記載の有効化リンクをクリックしてアカウントを認証してください。"
      redirect_to root_url
    else
      render "new", status: :unprocessable_entity
    end
  end

  def update
    #----------------- 追記 -------------------
    @user.avatar.attach(params[:user][:avatar]) if @user.avatar.blank?
    #-----------------------------------------
    if @user.update(user_params)
      flash[:notice] = "プロフィールが変更されました"
      redirect_to @user
    else
      render "edit", status: :unprocessable_entity
    end
  end
 ・・・
  private
    def user_params
      #-------------------------------- ↓↓ :avatar を追記 -------------------------------------------------------
      params.require(:user).permit(:name, :gender, :avatar, :email, :content, :password, :password_confirmation)
    end
  ・・・
end

createアクション、updateアクションにそれぞれ以下の1行

@user.avatar.attach(params[:user][:avatar])

を追加することで、アップロードされた画像を@userオブジェクトにアタッチ(紐付け)します。

ここで、updateアクションにてif @user.avatar.blank?を付け加えているのはなぜか。

理由は、編集時にファイル(画像)を未選択の状態で「更新する」と、未選択の状態(画像がない状態)を上書きしてしまい、結果的に登録された画像が消えてしまうからです。

これはおそらく、

@user.avatar.attach(params[:user][:avatar])

params[:user][:avatar]で未選択(画像がない=nil)の状態を紐付けしてしまうことで、編集前に紐付けしていた元の画像から(画像がないという状態に)上書きしてしまうために起こった問題と考えられます。

画像が未選択の場合、params[:user][:avatar] = nilとなる。このまま更新してしまうと、@user.avatarnilが紐付いて画像が登録されていない状態(nil)となる。

そこで、

@user.avatar.attach(params[:user][:avatar]) if @user.avatar.blank?

とすることで、画像が既に登録されている場合は「未選択(nil)」の状態を@userオブジェクトに紐付けしないようにします。

variantを有効化する

variantメソッドを使って画像処理を行うためには、ImageMagickのインストール、およびmini_magickのgemをインストールする必要があります。

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

$ brew install imagemagick

# M1 MacでRosetta2ターミナル使用の場合
$ arch -x86_64 brew install imagemagick

M1 MacでRosetta2ターミナル使用の場合、homebrewのインストール先(/usr/local/以下)によっては$ brew install imagemagickで以下のエラーが出ることがあります。

Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)!
Please create a new installation in /opt/homebrew using one of the
"Alternative Installs" from:
  https://docs.brew.sh/Installation
You can migrate your previously installed formula list with:
  brew bundle dump

このようなエラーが出た場合、頭にarch -x86_64を付け加えてインストールするとうまくいきます。

インストールが終わったらバージョンを確認してみましょう。

$ convert --version
Version: ImageMagick 7.1.0-44 Q16-HDRI x86_64 20294 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(5.0) 
Delegates (built-in): bzlib fontconfig freetype gslib heic jng jp2 jpeg lcms lqr ltdl lzma openexr png ps raw tiff webp xml zlib
Compiler: gcc (4.2)        

次に、Gemfileに以下を追記します。

gem 'mini_magick'

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

$ bundle install

また、config/application.rbに以下の1文を追記します。

・・・
module HomestayMatching
  class Application < Rails::Application
  ・・・
 config.active_storage.variant_processor = :mini_magick
  end
end

gemのインストールを反映させるためにRailsサーバーを再起動させます。

これでvariantメソッドを使う準備は完了です。

viewを作成

  • 新規、編集用テンプレート(_form.html.erbなど)
  • 表示用テンプレート(show.html.erbなど)

この2種類のviewを作成していきます。

まずは新規、編集用のテンプレートから見ていきましょう。

・・・

<!----- プロフィール画像 ------>
<div class="mt-3 mb-5 <%= @user.errors.include?(:avatar) ? "validation_errors" : "" %>">
  <!------ 画像の添付 ------>
  <%= f.label :avatar, '画像(5MBまで)', class: "form-label fw-bold" %><br>
  <%= f.file_field :avatar, onchange: "avatarImage(this);", accept: "image/jpeg,image/gif,image/png", class: "mb-3" %>
  <%= render 'layouts/error_messages',class: "invalid-feedback", obj: @user, key: :avatar %>
  <!------ 添付画像のプレビューを表示 ------>
  <div id="avatar" >
    <img id="avatar_preview" class="mb-3" width="100">
  </div>
  <!------ 現在DBに登録(保存)されている画像を表示 ------>
  <% if @user.avatar.present? %> <!-- @user.avatarが存在する場合のみ image_tag を使用(if文ないとエラーが出る) -->
    <span><b>[現在登録されている画像]</b></span>
    <%= image_tag @user.avatar, width: 200, class: "mb-2" %>
  <% end %>
</div>

・・・

<!------ プレビュー表示用の JavaScript ------->
<script>
  function avatarImage(obj){
      var fileReader = new FileReader();
      fileReader.onload = (function() {
        document.getElementById('avatar_preview').src = fileReader.result;
      });
      fileReader.readAsDataURL(obj.files[0]);
  }
</script>

ここでは、

  • file_fieldで画像ファイルをアップロードするためのフォームを設置
  • file_fieldに添付した画像のプレビューを表示(Javascript)
  • image_tagで現在DBに保存されている画像を表示

の処理を記述しています。

<%= f.file_field :avatar, onchange: "avatarImage(this);", accept: "image/jpeg,image/gif,image/png", class: "mb-3" %>

ここで、file_fieldのonchange:で画像が添付された時にJavascriptの関数avatarImage(obj)が発火するようにしています。

accept: "image/jpeg,image/gif,image/png"では、ファイル選択ウィンドウにて指定した拡張子のデータのみアップロードできるようにしています。

続いて、表示用のテンプレートにimage_tagを以下を記述すると画像を表示することができます。

<%= image_tag @user.avatar, width: 200, class: "mb-2" %>

画像処理用のvariantメソッド用いると、画像のリサイズ、切り抜きなどを行うことができます。

<%= image_tag @user.avatar.variant(resize: "200x200^", gravity: "center", crop: "200x200+0+0"), class: "...", alt: "..." %>

上記のvariantでは、

  • resize: "200x200^"で画面上に表示する画像サイズを設定
  • gravity: "center"で基準点を真ん中に設定
  • 基準点を(0,0)として200x200サイズに切り抜く

の画像処理を実行します。

こうすることで、長方形の画像であっても200×200サイズの正方形の画像に変換することができます。

なお、画像を変更する場合は、編集時に別の画像を選択して更新することで新しい画像に変更(上書き)することができます。

アップロードした画像を削除する

アップロードした画像を削除する方法として、今回はチェックボックスを用いてチェックされた状態で更新すると削除するようにしていきます。

まず、view側には以下の内容を追記します。

・・・

<!----- プロフィール画像 ------>
<div class="mt-3 mb-5 <%= @user.errors.include?(:avatar) ? "validation_errors" : "" %>">

  ・・・

  <!------ 現在登録されている画像を表示(チェックしたものを削除する) ------>
  <% if @user.avatar.present? %>
    <span><b>[現在登録されている画像]</b></span>
    <p class="text-danger font09">※削除する場合は画像にチェックしてから更新してください</p><br>
    <div class="form-check">
      <%= f.check_box :avatar_id, {class: "form-check-input", id: "avatar-image-check"}, @user.avatar.id, false %>
      <label class="form-check-label" for="avatar-image-check">
        <%= image_tag @user.avatar, width: 200, class: "mb-2" %>
      </label>
    </div>
  <% end %>
</div>

・・・

以下のcheck_boxメソッド、

<%= f.check_box :avatar_id, {class: "form-check-input", id: "avatar-image-check"}, @user.avatar.id, false %>

では、チェックボックスがチェックされるとparams[:user][:avatar_id]@user.avatar.idの値が代入されます。

あとは、controller側でparams[:user][:avatar_id]に値がある場合は画像を削除(purgeメソッド使用)するよう記述します。

class UsersController < ApplicationController
 ・・・

  def update
    @user.avatar.attach(params[:user][:avatar]) if @user.avatar.blank?
    #----------------- 追記 -------------------
    if params[:user][:avatar_id]
      @user.avatar.purge
    end
    #-----------------------------------------
    if @user.update(user_params)
      flash[:notice] = "プロフィールが変更されました"
      redirect_to @user
    else
      render "edit", status: :unprocessable_entity
    end
  end

 ・・・
end

以上です。

お疲れ様でした。

参考資料

Qiita
ActiveStorage 画像プレビュー - Qiita 前提条件・ruby 2.6.6・rails 6.0.3.4・ActiveStorageをインストール済・deviseを使用・画像投稿システムを実装済概要 画像を投稿する際、特にデフォル…
Qiita
Active Storageで複数画像の投稿・削除 - Qiita #環境Ruby 2.5.1Rails 5.2.4.3#やりたい事Rails標準のファイル管理機能Active Storageを使い、画像を複数を投稿・削除できる機能を実装したい。###まず…
Qiita
【Rails + JavaScript】投稿画面に画像プレビュー機能を実装しよう! - Qiita 画像を投稿する際に選択した画像がプレビューできる機能を実装していきます。今回も初心者向けにレシピ投稿アプリを例に作成していきます。JavaScript初心者にもわかりやす...
Proぐらし(プロぐらし)
【Rails】ActiveStroageの使い方を実例で解説|複数画像やファイルのアップロードと個別に削除・変更する方... Ruby on Railsに用意されている便利機能の一つにActive Storageがあります。Active Storageを使うと画像やPDFなどのサイズの大きなファイルの取り扱いをすることができます...
Proぐらし(プロぐらし)
【Rails】ActiveStorageのvariantを使いこなす!便利な画像変換のメソッドやオプションを実例で解説(!, >,... RailsのActiveStorageを使うと、既存のレコードに画像を紐づけることができます。 その画像に対してvariantメソッドを使うと、画像のサイズ変更や回転などの処理を加えるこ...
Qiita
【Rails】画像をアップロードせずに編集するとデフォルトの画像で上書きされてしまう問題の解決方法【Activ... 現在、個人開発中のアプリがかなり完成に近づきつつある中、新たに新しいバグを見つけてしまいました、、(リリース前に気付いてよかった)前置き長いんで、さっさと解決策...
MDN Web Docs
FileReader.readAsDataURL() - Web API | MDN readAsDataURL メソッドは、指定されたBlob または File の内容を読み込むために使用されます。読み込み操作が終了すると、readyState が DONE となり、loadend が発生しま...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

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

コメント

コメントする

目次