ユーザーのプロフィール画像(アバター)を作成するにあたり、
- ActiveStorageでファイルから画像をアップロード
- プロフィール編集画面にてアップロードした画像のプレビュー
- variantによる画像のリサイズおよび切り抜き(画像登録時)
- アップロードした画像の削除
これらの処理を行う方法についてまとめてみました。
複数枚の画像アップロード(プレビュー機能あり)の実装手順については以下の記事をご覧ください。
開発環境
- 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)
表示するビューの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.avatar
にnil
が紐付いて画像が登録されていない状態(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
以上です。
お疲れ様でした。
コメント