Active Storageの仕組み(その1)
今回から何回かに分けてActive Storage1の仕組みを紐解いてみましょう。 さて、早速ですが公式ガイドにならいUser
がavatar
を持っているとします。
class User < ApplicationRecord
has_one_attached :avatar
end
avatar
を表示するにはimage_tag
にuser.avatar
を渡すだけです。
<%= image_tag user.avatar %>
すると以下のようなリンクが生成されます。
<img
src="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MSwicHVyIjoiYmxvYl9pZCJ9fQ==--22989801b78c27abf7bc8c8b023cac322a42fbea/dog.jpg"
/>
ここで連鎖的に疑問が生まれます。
-
user.avatar
を渡すと/rails/active_storage/blob/redirect/...
なるURLが生成されたのは何故か。 - Railsの規約に従うなら
/avatars/1
のようなURLが生成されるべきではないのか。 - そもそも
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_to
やimage_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::Attachment
はActiveRecord::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_id
2を呼び出している点です。これが生成された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
で使用されていたresolve
やdirect
は、日常的に使うものではありませんが、Active Storageではその仕組みを上手に使ってURLを生成していることが分かりました。Railsのroutingは上手に設定すると非常に便利です。改めて公式ガイド3を読むことをお勧めします。
長くなってしまったので今回はここまでとします。次回はActiveStorage::Blobs::RedirectController#show
の中身から続きを確認しましょう。