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で呼び出したい値を設定している。