0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

添付ファイルのリンク化とActiveStorageの基礎

0
Posted at

背景

  • ActiveStorage でアップロードしたファイルのダウンロードURLを発行したい、というシンプルな要望が来た

  • URL 生成のヘルパーを把握するついでに、ActiveStorage の Blob / Attachment / Attached という3層構造も学び直したのでまとめる

ActiveStorage の 3 層構造

ActiveStorage は「ファイル本体」と「紐付け情報」を分けて管理する

名前 役割 DB上の表現
Blob ファイル本体の情報を持つ active_storage_blobs テーブル
Attachment アプリのレコードと Blob を繋ぐ中間テーブル active_storage_attachments テーブル
Attached アプリ側から操作する窓口オブジェクト DB上の実体なし(関連メソッド経由)
[アプリのモデル]    [中間テーブル]              [ファイル管理]

UserAttachment ──── ActiveStorage::Attachment ──── ActiveStorage::Blob
                                                           │
                                                           ↓
                                                    実ファイル(S3 など)

なぜ本体とメタ情報を分けるのか

  • バイナリデータを DB に入れると DB が肥大化して遅くなる
  • 検索や絞り込みは DB 側でやりたい
  • ファイル本体は専用のストレージサービス(S3 など)に置く方が効率的

なぜ中間テーブルが必要なのか

  • 同じ Blob を複数モデルで使い回せる
  • 1つのモデルに複数種類の添付(アバター / ヘッダー / …)をカラム追加なしで持てる
  • どのレコードに何の名前で紐付いているかを柔軟に管理できる

ActiveStorage::Attachment の主要カラム:

カラム 内容
name "data_file" など、紐付けの役割名
record_type "UserAttachment" などモデル名
record_id 紐付くレコードのID
blob_id どの Blob を参照しているか

has_one_attached :data_file が生やすもの

1行書くと、Rails が裏で関連メソッドを自動生成する

class UserAttachment < ApplicationRecord
  has_one_attached :data_file
end
メソッド 返り値 用途
record.data_file Attached オブジェクト(窓口) 普段の操作(filename取得、attach、url生成等)
record.data_file_attachment ActiveStorage::Attachment レコード 中間テーブルへの直接アクセス
record.data_file_blob ActiveStorage::Blob レコード Blob レコードへの直接アクセス

has_one_attached :〇〇 と書くと、自動的に 〇〇_attachment〇〇_blob という関連名が作られる

data_file メソッドが返す Attached オブジェクトは DB に対応する実体を持たず、ファイル操作の窓口として機能する。filename / url / attach / purge などの便利メソッドはここから生えている

ダウンロードURL生成の3つのヘルパー

ActiveStorage は URL 生成用のヘルパーを複数提供している。用途に応じて使い分ける

ヘルパー 返り値 主な用途
rails_blob_url(blob) フルURL(https://... メール・外部通知・Slack投稿など、アプリ外に出すURL
rails_blob_path(blob) パスのみ(/rails/... 自分のアプリ内のビューで <a href="..."> に埋め込む
url_for(blob) デフォルトはパス、only_path: false でフルURL 汎用ヘルパー。型に応じてポリモーフィックに解決

rails_blob_url

url = rails_blob_url(attachment.data_file)
# => "https://example.com/rails/active_storage/blobs/redirect/eyJfcmFpb.../report.pdf"
  • フルURLが要る場面で使う
  • 内部にホスト名を含めるため、Rails.application.routes.default_url_options[:host]config.action_mailer.default_url_options でホスト設定が必須
  • 設定が抜けると ArgumentError: Missing host to link to! で落ちる

rails_blob_path

path = rails_blob_path(attachment.data_file)
# => "/rails/active_storage/blobs/redirect/eyJfcmFpb.../report.pdf"
  • ホスト設定不要なので楽
  • 外部送信(メールや外部通知)には使えない

url_for(落とし穴あり)

url_for(attachment.data_file)
# => "/rails/active_storage/blobs/redirect/.../report.pdf"   ← パス!

Rails 公式ドキュメントによれば url_for のデフォルトは only_path: true。つまりビュー内で呼ぶとパスを返す。フルURLが欲しい場合は明示する:

url_for(attachment.data_file, only_path: false)
url_for(attachment.data_file, host: 'example.com')

url_for がそもそも Blob を受け取れるのは、ActiveStorage 側がポリモーフィックルーティングに対応しているため。内部的には rails_blob_path / rails_blob_url に解決される

ただし「動く」のと「分かりやすい」のは別問題。ActiveStorage 専用の場面では rails_blob_url / rails_blob_path を直接呼ぶ方が意図が明確url_for は引数の型で挙動が変わるため、コードを読む人が「これ何を返す?」と迷う

URLの構造

https://example.com/rails/active_storage/blobs/redirect/eyJfcmFpb.../report.pdf
└──────┬──────┘ └──────────────┬──────────────────┘ └──┬──┘ └────┬───┘
       ①                        ②                       ③         ④
部位 中身 役割
① ホスト アプリのドメイン どのRailsに問い合わせるか
② パス /rails/active_storage/blobs/redirect/ ActiveStorage 専用ルート
③ 署名トークン 長い英数字列 Blob IDと有効期限を暗号化した署名
④ ファイル名 report.pdf ブラウザのダウンロード表示用

③の署名トークンに「どのBlobか」「いつまで有効か」が暗号化されて入っている。改ざんしたら検証で弾かれる。これが「署名付きURL」と呼ばれる所以。

redirect モードの動き(デフォルト)

ユーザーが上のURLをクリックすると、こうなる:

  1. Rails が署名を検証、有効期限チェック
  2. Blob から S3 の保存先キーを取得
  3. S3 に対して短期署名付きURLを発行
  4. ブラウザに「S3のURLにリダイレクトせよ」と返す(HTTP 302)
  5. ブラウザは S3 のURLに再アクセスしてダウンロード

つまり Rails サーバーは「鍵」を渡すだけで、実ファイルは S3 から直接流れる設計。Rails サーバーに負荷がかからない

URL生成の実装上の注意

concern/ジョブ内ではフルパスで呼ぶ

URL ヘルパーは Rails.application.routes.url_helpers モジュールに住んでいる。コントローラやビューでは Rails が自動で include しているが、モデル・ジョブ・concern には include されていない

# ❌ そのままではエラー: undefined method `rails_blob_url`
rails_blob_url(attachment.data_file)

# ✅ フルパスで呼ぶ
Rails.application.routes.url_helpers.rails_blob_url(attachment.data_file)

concern で何度も使うなら include する手もある:

module Linkable
  extend ActiveSupport::Concern
  include Rails.application.routes.url_helpers
end

ホスト設定

フルURLを生成するには、Rails に自分のホスト名を教えておく必要がある。

# config/environments/production.rb
Rails.application.routes.default_url_options[:host] = 'example.com'
Rails.application.routes.default_url_options[:protocol] = 'https'

URLの有効期限

デフォルトの署名URL有効期限は5分。ジョブで生成→数時間後にユーザーがクリック、というケースでは短すぎることがある

# config/initializers/active_storage.rb
Rails.application.config.active_storage.urls_expire_in = 12.hours

期限の設計判断:

設定値 利点 欠点
5分(デフォルト) URL漏洩リスク最小 非同期通知経由で切れることがある
12時間 半日以内のクリックに対応 漏れたら半日アクセス可能
7日 長期メールでも対応 リスク大、推奨せず

業務系アプリなら 1〜24 時間 が現実的な範囲

N+1 問題と includes

添付ファイル付きのレコードを一覧表示するとき、何も考えずに書くと N+1 問題が発生する

includes なしの場合(添付が3個ある時):

クエリ1: 添付ファイル一覧(3件)
クエリ2: 添付1の中間テーブル
クエリ3: 添付1の Blob
クエリ4: 添付2の中間テーブル
...

→ 添付数に比例してクエリが増える

includes(data_file_attachment: :blob) を入れると:

クエリ1: 添付ファイル一覧
クエリ2: 関連する中間テーブルを一括取得
クエリ3: 関連する Blob を一括取得

3クエリで完結する

includes には DB 上の関連名を指定する

# ✅ 正しい
.includes(data_file_attachment: :blob)

# ❌ 動かない
.includes(data_file: :blob)

data_file は窓口メソッド(Attached オブジェクトを返す Ruby メソッド)であり、ActiveRecord の関連としては存在しない。includes は関連を辿る機能なので、DB 上に存在する関連名を指定する必要がある

具体的には data_file_attachment(中間テーブルへの has_one 関連)と、data_file_blob(through 経由で Blob を取る関連)の2つが自動定義されている

まとめ

  • ActiveStorage は「ファイル本体」と「紐付け情報」を分けて管理する3層構造(Blob / Attachment / Attached)
  • has_one_attached :data_file 1行で、窓口メソッド + 中間テーブル関連 + Blob 関連が自動生成される
  • ダウンロードURL生成は用途別に3つのヘルパーがある
    • 外部送信なら rails_blob_url(フルURL)
    • アプリ内ビューなら rails_blob_path(パス)
    • url_for はデフォルト only_path: true なので、フルURLが欲しい場合は明示が必要
  • concern/ジョブ内で URL ヘルパーを呼ぶときは Rails.application.routes.url_helpers. を前置
  • 一覧表示のN+1対策には includes(data_file_attachment: :blob) を使う(窓口メソッド名 data_file は使えない)

感想

has_one_attached の裏で何が起きてるのか、ずっとフワッとした理解のままだったので良かった

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?