今回から何回かに分けてActive Storage1の仕組みを紐解いてみましょう。 さて、早速ですが公式ガイドにならいUseravatarを持っているとします。

class User < ApplicationRecord
  has_one_attached :avatar
end

avatarを表示するにはimage_taguser.avatarを渡すだけです。

<%= image_tag user.avatar %>

すると以下のようなリンクが生成されます。

<img
  src="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MSwicHVyIjoiYmxvYl9pZCJ9fQ==--22989801b78c27abf7bc8c8b023cac322a42fbea/dog.jpg"
/>

ここで連鎖的に疑問が生まれます。

  1. user.avatarを渡すと/rails/active_storage/blob/redirect/...なるURLが生成されたのは何故か。
  2. Railsの規約に従うなら/avatars/1のようなURLが生成されるべきではないのか。
  3. そもそもuser.avatarとは何か。

順番に調べてみましょう。まずはuser.avatarの実態は何かを調べます。

irb> User.first.avatar
#<ActiveStorage::Attached::One:0x00007ff680e052a8
 @name="avatar",
 @record=
  #<User:0x00007ff68104ab58>

ActiveStorage::Attached::Oneが返却されました。

※以下より後に抜粋するRailsのコードは説明のために大幅に編集・簡略化してあります。

module ActiveStorage
  class Attached
  end

  class Attached::One < Attached
  end
end

どうやらActiveStorage::Attached::OneはPORO(Plain Old Ruby Object)のようです。言い換えるとActive Recordを継承している訳ではないので、link_toimage_tagに渡すとエラーになりそうなものです。実際にPOROをlink_toに渡すと以下のエラーが発生します。

class Post # ApplicationRecordから継承してないことに注意。
end
<%= link_to "Post", Post.new %>
#=> undefined method `to_model' for #<Post:0x00007fe354a9aeb8>

ではPOROであるにも関わらずActiveStorage::Attached::Oneがエラーにならないのは何故でしょうか。中身をもう少し掘り下げてみましょう。

module ActiveStorage
  class Attached::One < Attached
    delegate_missing_to :attachment
  end
end

どうやらdelegate_missing_to大体の処理をattachmentに委譲しているようです。そこでattachmentの正体を探ってみましょう。

irb> User.first.avatar.attachment
=> #<ActiveStorage::Attachment:0x00007f8f12b568e8 id: 1, name: "avatar", record_type: "User", record_id: 1, blob_id: 1>

attachmentの正体はActiveStorage::Attachmentだと分かりました。これの実装を確認してみましょう。

class ActiveStorage::Attachment < ActiveStorage::Record
end

class ActiveStorage::Record < ActiveRecord::Base
  self.abstract_class = true
end

ActiveRecord::AttachmentActiveRecord::Baseを継承していました。本丸に近付いて来たようですが、やはり生成されるURLがRailsの規約に従っておらず、疑問が残ります。

<!-- 実際に生成されたURL -->
<img
  src="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MSwicHVyIjoiYmxvYl9pZCJ9fQ==--22989801b78c27abf7bc8c8b023cac322a42fbea/dog.jpg"
/>

<!-- Railsの規約に従えば生成されるであろうURL -->
<img src="/active_storage/attachments/1" />

さて、最後の鍵はActive Storageのroutes.rbにあります。

resolve("ActiveStorage::Attachment") do |attachment, options|
  route_for(:rails_storage_redirect, attachment.blob, options)
end

上記によりlink_toなどにActiveStorage::Attachmentが渡された場合、rails_storage_redirectを呼び出すようにしています。これがRailsの規約ではないURLが生成されていた理由です。では次にrails_storage_redirectの中身を見ましょう。

direct :rails_storage_redirect do |model, options|
  route_for(:rails_service_blob, model.signed_id)
end

これもrails_service_blobを呼び出しているだけですが、注目すべき点が1つあります。それは呼び出しの際にmodel.signed_id2を呼び出している点です。これが生成されたURLにランダムな文字列が含まれていた理由です。実際にsigned_idを呼び出してみると、URLの文字列と一致してることが分かります。

irb> User.first.avatar.attachment.blob.signed_id
#=> "eyJfcmFpbHMiOnsiZGF0YSI6MSwicHVyIjoiYmxvYl9pZCJ9fQ==--22989801b78c27abf7bc8c8b023cac322a42fbea"

最後にrails_service_blobですが、これはActiveStorage::Blobs::RedirectController#showを呼び出しているだけです。この部分の記法は馴染み深いでしょう。

get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob

さて、これにてuser.avatarからURLが生成される仕組みを解明出来ました。routes.rbで使用されていたresolvedirectは、日常的に使うものではありませんが、Active Storageではその仕組みを上手に使ってURLを生成していることが分かりました。Railsのroutingは上手に設定すると非常に便利です。改めて公式ガイド3を読むことをお勧めします。

長くなってしまったので今回はここまでとします。次回はActiveStorage::Blobs::RedirectController#showの中身から続きを確認しましょう。