ActiveStorage周りのコードを読んでみる

前提
- Ruby: v3.1.2
- Ruby on Rails: v7.1.2

create_table "users", force: :cascade do |t|
t.string "name"
t.string "address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
こんなかんじでUsersテーブルを定義して、
class User < ApplicationRecord
has_one_attached :avatar
end
モデルを定義

def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
validate_service_configuration(name, service)
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
# frozen_string_literal: true
def #{name}
@active_storage_attached ||= {}
@active_storage_attached[:#{name}] ||= ActiveStorage::Attached::One.new("#{name}", self)
end
def #{name}=(attachable)
attachment_changes["#{name}"] =
if attachable.nil? || attachable == ""
ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
else
ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
end
end
CODE
has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy, strict_loading: strict_loading
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading
scope :"with_attached_#{name}", -> {
if ActiveStorage.track_variants
includes("#{name}_attachment": { blob: { variant_records: { image_attachment: :blob } } })
else
includes("#{name}_attachment": :blob)
end
}
after_save { attachment_changes[name.to_s]&.save }
after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
reflection = ActiveRecord::Reflection.create(
:has_one_attached,
name,
nil,
{ dependent: dependent, service_name: service },
self
)
yield reflection if block_given?
ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
end
-
has_one_attached
から読んでみる

-
validate_service_configuration(name, service)
で config.active_storage.service = :local とかが設定されているかチェック

-
generated_association_methods
とは?
def generated_association_methods # :nodoc:
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
private_constant :GeneratedAssociationMethods
include mod
mod
end
end
-
GeneratedAssociationMethods
というプライベート定数をセットして@generated_association_methods
に代入
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
# frozen_string_literal: true
def #{name}
@active_storage_attached ||= {}
@active_storage_attached[:#{name}] ||= ActiveStorage::Attached::One.new("#{name}", self)
end
def #{name}=(attachable)
attachment_changes["#{name}"] =
if attachable.nil? || attachable == ""
ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
else
ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
end
end
CODE
- つまりここでattacheするファイルのゲッターとセッターを定義している
- この中の
__FILE
,__LINE__
については https://stackoverflow.com/questions/2496102/what-does-class-eval-end-eval-file-line-mean-in-ruby に説明があった

has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy, strict_loading: strict_loading
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading
associationを定義

scope :"with_attached_#{name}", -> {
if ActiveStorage.track_variants
includes("#{name}_attachment": { blob: { variant_records: { image_attachment: :blob } } })
else
includes("#{name}_attachment": :blob)
end
}
- scopeも定義
- N+1を回避するためのスコープ

after_save { attachment_changes[name.to_s]&.save }
after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
- save後にファイル名の保存、commit後にファイルをアップロードしている

reflection = ActiveRecord::Reflection.create(
:has_one_attached,
name,
nil,
{ dependent: dependent, service_name: service },
self
)
- ActiveRecord::Reflectionのインスタンスを作成している
ActiveRecord::Reflectionについて
Reflection enables the ability to examine the associations and aggregations of Active Record classes and objects. This information, for example, can be used in a form builder that takes an Active Record object and creates input fields for all of the attributes depending on their type and displays the associations to other objects.
MacroReflection class has info for AggregateReflection and AssociationReflection classes.
https://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html より引用
そのモデルに定義されてるassociationやdelegationを集約してくれるクラスっぽい
例えば
class User < ApplicationRecord
has_many :profiles
end
class Profile < ApplicationRecord
belongs_to :user
end
こんなassociationが定義されているとする。
rails consoleで試してみると
$ bundle exec rails c
Loading development environment (Rails 7.1.2)
irb(main):001> User.reflect_on_association(:profiles)
=>
#<ActiveRecord::Reflection::HasManyReflection:0x0000000106f63838
@active_record=User (call 'User.connection' to establish a connection),
@association_foreign_key=nil,
@association_primary_key=nil,
@class_name=nil,
@counter_cache_column=nil,
@foreign_key=nil,
@inverse_of=nil,
@inverse_which_updates_counter_cache=nil,
@inverse_which_updates_counter_cache_defined=false,
@join_table=nil,
@klass=nil,
@name=:profiles,
@options={},
@plural_name="profiles",
@scope=nil>
こんな感じでassociationの情報を返してくれる

yield reflection if block_given?
ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
blockが定義されていたらそれをyieldする。
作成したActiveRecord::Reflectionインスタンスに対して、ActiveStorageで定義したadd_attachment_reflection
を呼んでいる

def add_attachment_reflection(model, name, reflection)
model.attachment_reflections = model.attachment_reflections.merge(name.to_s => reflection)
end
attachment_reflections
というHashにnameとreflectionのHashをmergeしてmodel.attachment_reflections
に代入している

module ActiveRecordExtensions
extend ActiveSupport::Concern
included do
class_attribute :attachment_reflections, instance_writer: false, default: {}
end
module ClassMethods
# Returns an array of reflection objects for all the attachments in the
# class.
def reflect_on_all_attachments
attachment_reflections.values
end
# Returns the reflection object for the named +attachment+.
#
# User.reflect_on_attachment(:avatar)
# # => the avatar reflection
#
def reflect_on_attachment(attachment)
attachment_reflections[attachment.to_s]
end
end
end
この辺りをみるとattachment_reflections
がなんぞやと言うのはなんとなくわかる。
reflect_on_all_attachments
とかreflect_on_attachment
で呼び出したい値を設定している。