😎

ActiveStorage でグローバルな画像を扱う

に公開

はじめに

弊社では Ruby on Rails を使用しており、ActiveStorage を用いて画像やファイルを扱っています。
BtoBtoC のサービスを運営しているので、エンドユーザには CDN を通して配信し、顧客企業向けには S3 の署名付き URL を発行してアクセスを制限した配信を行っています。S3 には CDN 用と署名付き URL 用の 2 種類のバケットを用意しています。

先日ある機能を開発しているときに、全顧客で同一の画像(グローバルな画像)をエンドユーザ向けに配信したいという要件が出てきました。具体的な要件は次の通りです。

  • エンドユーザ向けの画像なので、CDN を通して配信する
  • 全顧客で同一のグローバルな画像を使用する
  • 今回必要な画像は 1 件のみ
  • 将来的に画像が増える可能性はあるが、現時点では不明

通常の場合だと画像はモデルにアタッチして扱いますが、今回はグローバルな画像なので普段とは異なる方法を検討しました。

この記事ではグローバルな画像を扱う際に ActiveStorage の一般的な方法の懸念点や、検討した方法をまとめます。

一般的な方法(案1: 既存のモデルにアタッチする)

全顧客で同じ画像を扱うので、既存のモデル(e.g. User)にアタッチする方法をまず考えました。同じ画像を扱いたかったので、1レコードにアタッチした画像を使い回したいです。
具体的には以下のようなイメージです。

class User < ApplicationRecord
  has_one_attached :image, service: :cdn # 既存のモデルにアタッチ
end

# 1レコードに画像をアタッチ
user = User.first
user.image.attach(
  io: File.open('path/to/image.png'),
  filename: 'image.png',
  content_type: 'image/png'
)
# 全レコードにアタッチ
blob = user.image.blob
User.each { |user| user.image.attach(blob) }

# 画像を使う時は以下のように取得
User.first.image.url

しかしこちらの方法では将来画像を変更したくなったときに全レコードに対してバックフィルが必要になります。また画像の種類が増えたときにどうするかも悩ましいです。
そもそもグローバルな画像なので、既存のモデルにアタッチする必要があるのかとも思いました。

そこで他に良い方法がないか検討してみました。

他に検討した方法

  • 案2: グローバルな情報を扱う専用のモデルを作り、そのモデルにアタッチする
  • 案3: 特定のレコードに紐付けず ActiveStorage::Blob のみを使う
  • 案4: ActiveStorage を使わず S3 に直接画像をアップロードし、画像の URL を settings などに置く

案2: グローバルな情報を扱う専用のモデルを作り、そのモデルにアタッチする

既存のモデルを使うのではなく、グローバルなデータを扱う専用のモデル(e.g. Masterdata)を作成してそのモデルにアタッチする方法を考えました。

# モデル自体を新規作成
class Masterdata < ApplicationRecord
  has_one_attached :image, service: :cdn
end

# レコードを作成し、画像をアップロード
Masterdata.create!(name: 'Feature').image.attach(
  io: File.open('path/to/image.png'),
  filename: 'image.png',
  content_type: 'image/png'
)

# 画像を使う時は以下のように取得
Masterdata.find_by!(name: 'Feature').image.url

こちらの方法ではアタッチするレコードは1件のみなので、画像の変更や削除が簡単です。また、画像の種類が増えた場合も Masterdata にカラムを追加することで対応できます。

ただ懸念点としては専用モデルを作ったとしても他のレコードが増える見込みが不明なので、今そこまでやる必要があるのかは悩ましいところです。

案3: 特定のレコードに紐付けず ActiveStorage::Blob のみを使う

ActiveStorage::Blob は ActiveStorage の内部で使用されているモデルで、アップロードされたファイルの情報を保持しています。URL を取得するメソッドも ActiveStorage::Blob が持っているので、ActiveStorage::Blob のみを使って画像を扱うことができそうです。

調べたところ ActiveStorage::Blob.create_and_upload! メソッドを使うことで、既存のレコードに紐付けずに画像をアップロードすることができます。

https://github.com/rails/rails/blob/33beb0a38db1c058123a8e3cc298cad918adfe32/activestorage/app/models/active_storage/blob.rb#L91-L100

使用イメージは以下のようになります。

# 画像をアップロード
ActiveStorage::Blob.create_and_upload!(
  key: 'Feature', # 任意の値を指定
  io: File.open('path/to/image.png'),
  filename: 'image.png',
  content_type: 'image/png',
  service_name: 'cdn' # CDN 用のサービスを指定
)

# 画像を使う時は以下のように取得
ActiveStorage::Blob.find_by!(key: 'Feature').url

既存のモデルを作成する必要がないので、実装がシンプルです。将来的に画像を増やしたくなった時も key を変えることで対応できます。

ただ懸念点としては画像を使わなくなった時に、blob のレコードを削除するのを忘れそうです。

案4: ActiveStorage を使わず S3 に直接画像をアップロードし、画像の URL を settings などに置く

最後に ActiveStorage を使わずに S3 に直接画像をアップロードし、画像の URL を settings などに置く方法を考えました。

# 画像を使うとき
settings.feature_image_url

モデル名などを気にせずに済むので、実装はシンプルです。画像の追加や削除も簡単です。

ただし現在の S3 の構成を考えたところ、新規にバケットを作成する必要がありました。今後画像が増えるかどうか不明なため、そこまでやる必要があるのかは悩ましいところです。

今回採用した方法

今回は 案3: 特定のレコードに紐付けず ActiveStorage::Blob のみを使う を採用しました。
採用した理由は以下の通りです。

  • 実装がシンプルで、実装コストが低い
    • 現状必要な画像は1件のみで今後増えるかどうかも不明なため、実装コストを優先
  • 将来的に 案1,2 の方法に移行しやすい
    • 既存のモデルにアタッチする場合は blob を使い回すことができる

環境や要件によって最適な方法は異なりますが、参考になれば幸いです。

GitHubで編集を提案
Social PLUS Tech Blog

Discussion