Open12

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

加藤雅人加藤雅人
  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

モデルを定義

加藤雅人加藤雅人
active_storage/attached/model.rb
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とは?
active_record/core.rb
      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に代入
active_storage/attached/model.rb
        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
加藤雅人加藤雅人
active_storage/attached/model.rb
        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を定義

加藤雅人加藤雅人
active_storage/attached/model.rb
        after_save { attachment_changes[name.to_s]&.save }

        after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
  • save後にファイル名の保存、commit後にファイルをアップロードしている
加藤雅人加藤雅人
active_storage/attached/model.rb
        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の情報を返してくれる

加藤雅人加藤雅人
active_storage/attached/model.rb
        yield reflection if block_given?
        ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)

blockが定義されていたらそれをyieldする。

作成したActiveRecord::Reflectionインスタンスに対して、ActiveStorageで定義したadd_attachment_reflectionを呼んでいる

加藤雅人加藤雅人
active_storage/reflection.rb
      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に代入している

加藤雅人加藤雅人
active_storage/reflection.rb
    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で呼び出したい値を設定している。