ActiveStorage で画像アップロードを実装

今回はRails5.2から標準搭載のファイルアップローダーであるActiveStorageを使い、画像アップロード機能を実装しましたのでまとめです。 AWS S3などのクラウドストレージへのアップロードも設定で手軽に実装できるとのことですが、今回はクラウドストレージへのアップロードは実装しておりません。

Active Storage の概要

学んだこと✏️

1. Active Storageをセットアップ

以下を実行し、ActiveStorageでアップロードファイルを管理するテーブルのmigrationファイルを生成。

$ rails active_storage:install

db/migrate/[timestamp]_create_active_storage_tables.active_storage.rbが作成されるので$ rails db:migrateを実行。

  • rails db:migrateで生成されるテーブル
    • active_storage_blobsテーブル...アップロードされたファイル自体のメタデータを格納。(ファイルの名前、種類、サイズ、SHA256ハッシュ、作成日時、更新日時など) Active Storageはこのテーブルに格納されたメタデータを使用し、アプリ内でのファイルのアップロード、ダウンロード、削除などを処理。ファイル自体はActive Storageが対応するストレージサービスにアップロードされ、そのURLがactive_storage_blobsテーブルに保存される。その為、アプリから容易にファイルを取り扱える。

    • active_storage_attachmentsテーブル...Active Storageの中で、実際のアップロードされたファイルの保存場所を示すために使用。アップロードされたファイルをどのレコードにアタッチしたかを記録。

例)ユーザーが投稿した画像を、Postモデルに紐付ける場合、active_storage_attachmentsテーブルには、Postモデルの id と、アップロードされた画像のidが保存される。 blob_idカラムはactive_storage_blobsテーブルに保存されているアップロードされたファイルのメタデータを参照するために使用。 つまり、アップロードされたファイルと、それがどのレコードにアタッチされたかを追跡するために使用される。このテーブルの情報を使い、アプリケーション内でアップロードされたファイルを管理することができる。

  • アタッチ【attach】...システム上で何らかの主体に対象を取り込んで有効にする動作や操作。
2.モデルにActiveStorageの関連付けを追加

各userにavatar画像を持たせたい場合、以下のようにUserモデルを定義。

  • has_one_attached...レコード1件ごとに1個のファイルを添付できる。
  • has_many_attached...レコード1件ごとに、多数の添付ファイルを添付できる。
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one_attached :avatar
end

以下でavatar画像をアップロード。

<div class="field">
  <%= f.label :avatar %>
  <%= f.file_field :avatar %>
</div>

以下でavatar画像を表示。

<%= image_tag user.avatar if user.avatar.attached? %>
  • 生成されたビューの送信ボタンのすぐ上にfile_fieldを追加。アップロードした画像をimage_tagで表示。attached?で存在チェック。
3.バリアントプロセッサをVipsへ変更

▶︎バリアントプロセッサ

画像を処理するための機能を提供する仕組み。

  1. サイズの変更 アップロードされた画像のサイズを変更して、サムネイルやプレビュー画像を生成することができる。

  2. 形式の変換 アップロードされた画像を別のファイル形式に変換することができる。たとえば、JPEG形式からPNG形式に変換することができる。

  3. フィルタリング アップロードされた画像に対して、ぼかしやシャープネスなどのエフェクトを適用することができる。

    • バリアントプロセッサは、Active Storageのvariantメソッドを使用して呼び出すことができる。バリアントは、必要に応じてストレージに保存され、再度使用される際にはバリアントの生成コストを削減するためにキャッシュされる。

  • MiniMagick...ImageMagickと呼ばれる画像処理ツールをRubyから使用するためのライブラリであり、簡単にリサイズ、トリミング、変換などの画像操作ができる。

  • Vips...MiniMagickよりも高速で、メモリ使用量が少なく、大規模な画像処理をより効率的に行うことができます。

    • MiniMagickVipsは画像処理ツール。ActiveStorageはファイル管理フレームワークActiveStorageMiniMagickまたはVipsなどの画像処理ツールを使用して、アップロードされた画像を処理することができる。

Active StorageのデフォルトのバリアントプロセッサはMiniMagickの為、Vipsを指定。

画像プロセッサをVipsに切り替えるには、config/application.rbに以下を追加。

# 別の画像プロセッサとしてVipsを使う
config.active_storage.variant_processor = :vips

7 画像を変換する

3.16 Active Storageを設定する

4. 画像をリサイズ

Gemfileのコメントを解除。

# Use Active Storage variant
gem 'image_processing', '~> 1.2'

image_processinggemを使用するため、macimagemagickvipsをインストール。

$ brew install imagemagick vips

ImageProcessing

ビューで、画像を表示するために、variantメソッドを使用。これで添付ファイルごとに特定のサイズ違いの画像を生成できる。 resize_to_fillresize_to_limitresize_to_fitなどのオプションがある。適切なオプションを選択して、画像をリサイズする。

<%= image_tag user.avatar.variant(resize_to_fit: [50, 50]) if user.avatar.attached? %>

ImageProcessing::MiniMagick

resize_to_fit...image_processing gemが提供するActiveStorageの画像リサイズオプションの一つで、指定したサイズに画像を縮小することができる。 resize_to_fitは、指定したサイズ内に収まるように画像を比率を維持しながらリサイズするため、画像の横幅または縦幅が指定したサイズに合うようにリサイズされる。 元の画像のアスペクト比を維持しながらリサイズするため、画像の横幅または縦幅が指定したサイズよりも小さい場合は、元の画像のサイズのままになる。しかし、横幅または縦幅が指定したサイズよりも大きい場合は、指定したサイズに合わせてリサイズされる。 画像の比率を維持しながら指定したサイズに収まるようにリサイズするため、元の画像の一部が切り落とされる場合がある。また、縦横比が指定したサイズと異なる場合は、余白が追加されることになる。

補足:Active Storageを使用する際のログの情報
Started POST "/posts" for 127.0.0.1 at 2022-02-25 09:30:00 +0900
Processing by PostsController#create as HTML
  Parameters: {"post"=>{"title"=>"Example Post", "image"=>#<ActionDispatch::Http::UploadedFile:0x0000000000000000 @tempfile=#<Tempfile:/tmp/RackMultipart20220225-1234-5678abcdef>, @original_filename="example.jpg", @content_type="image/jpeg">}}
  ...
  ActiveStorage::Blob Create (0.1ms)  INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "byte_size", "checksum", "created_at") VALUES (?, ?, ?, ?, ?, ?)  [["key", "example.jpg"], ["filename", "example.jpg"], ["content_type", "image/jpeg"], ["byte_size", 123456], ["checksum", "0123456789abcdef"], ["created_at", "2022-02-25 09:30:01.000000"]]
  ...
  ActiveStorage::Attachment Create (0.1ms)  INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES (?, ?, ?, ?, ?)  [["name", "image"], ["record_type", "Post"], ["record_id", 1], ["blob_id", 1], ["created_at", "2022-02-25 09:30:02.000000"]]
  ...
Redirected to http://localhost:3000/posts/1
Completed 302 Found in 123ms (ActiveRecord: 1.0ms | Allocations: 4567)
  • リクエストが開始された時間とIPアドレス
  • コントローラーのアクションが処理される際に、送信されたパラメーター
  • Active Storageが、アップロードされたファイルに対して行った操作の詳細(例:Blobオブジェクトの作成など)
  • Active Storageが、アップロードされたファイルを保存した際に生成されたAttachmentオブジェクトの詳細
  • リダイレクト先のURLと、リクエストが完了した際の処理時間とロケーションの数
5. N+1問題
  • Active Recordを使用する際に発生するパフォーマンスの問題。 Active Recordは、データベースから複数のレコードを取得するために使用。しかし、N+1問題は、複数のレコードを取得する際に、必要以上にデータベースにアクセスし、パフォーマンスを低下させる問題。 1つの親レコードに関連する複数の子レコードを取得する場合に発生する。この場合、最初に親レコードを取得し、その後に子レコードを取得する必要がある。しかし、Active Recordがデフォルトで使用する方法では、1つの親レコードを取得するために1回のデータベースクエリを実行し、その後、それぞれの子レコードを取得するためにさらにN回のデータベースクエリを実行する必要がある。合計N+1回のデータベースクエリを実行することになる。

→解決方法:with_attached_属性名スコープを使用することでN+1問題を回避しながら、関連するファイルを一度のクエリで取得できる。

app/controllers/users_controller.rbwith_attached_avatarとすることでN+1問題を回避する。

class UsersController < ApplicationController
  def index
    @users = User.with_attached_avatar.order(:id).page(params[:page])
  endend

N+1を解決する

まとめ

他にも、active_storage_validations gemを使用し、アップロード可能なファイルの種類を限定する方法などがあります。時と場合に応じて使い分けられるようにしていきたいです。