活动记录模型默认映射

Active Record Model Default Mapping

我是 Rails 的新手,我不明白的一件事是 rails 官方文档中说数据库默认映射到模型。

例如,如果我在迁移中将默认值放在列上,我希望在将特定记录保存到数据库时为该记录插入默认值。但是我注意到,当我执行 Record.new 时,模型属性已经具有在数据库中设置的默认值!这很有用,因为这意味着我不必在实例化新模型对象时显式设置它,但是在文档中的哪个位置说会自动设置新对象的默认值?

but where in the docs does it say this automatic setting of default on new objects happens?

没有。 ActiveRecord 如何神奇地读取数据库模式并在实例化一条新记录时从那里定义默认值的低级别实现分布在多个 APIs - 其中一些是内部的。如果您想详细了解它的工作原理,则需要深入研究代码。但是您实际上不需要知道它就可以编写 Rails 应用程序。

您真正需要知道的是,当 class 首次求值时,ActiveRecord 通过数据库适配器从数据库中读取模式。此架构信息缓存在 class 上,因此 AR 不必再次查询数据库并包含有关数据库列的类型和默认值的信息。

此信息随后用于在模型和属性上定义列缓存,这是一个非常分散的术语,用于描述关于属性存储的元数据、类型转换前后的值以及您用来访问的 setter 和 getter他们。不要被愚弄,这在任何方面都像一个简单的实例变量 - 你的 foo 属性没有存储在 @foo.

ActiveRecord/ActiveModel 知道在实例化模型时设置默认值,因为它会查看模型的属性。

ActiveRecord::ModelSchema 是一个内部 API 主要负责将数据库模式映射到模型上的列缓存:

# frozen_string_literal: true

require "monitor"

module ActiveRecord
  module ModelSchema
    # ...
    module ClassMethods
      # ....
      # Returns a hash where the keys are column names and the values are
      # default values when instantiating the Active Record object for this table.
      def column_defaults
        load_schema
        @column_defaults ||= _default_attributes.deep_dup.to_hash.freeze
      end

      # ...

      private
        def inherited(child_class)
          super
          child_class.initialize_load_schema_monitor
        end

        def schema_loaded?
          defined?(@schema_loaded) && @schema_loaded
        end

        def load_schema
          return if schema_loaded?
          @load_schema_monitor.synchronize do
            return if defined?(@columns_hash) && @columns_hash

            load_schema!

            @schema_loaded = true
          rescue
            reload_schema_from_cache # If the schema loading failed half way through, we must reset the state.
            raise
          end
        end

        def load_schema!
          unless table_name
            raise ActiveRecord::TableNotSpecified, "#{self} has no table configured. Set one with #{self}.table_name="
          end

          columns_hash = connection.schema_cache.columns_hash(table_name)
          columns_hash = columns_hash.except(*ignored_columns) unless ignored_columns.empty?
          @columns_hash = columns_hash.freeze
          @columns_hash.each do |name, column|
            type = connection.lookup_cast_type_from_column(column)
            type = _convert_type_from_options(type)
            warn_if_deprecated_type(column)
            define_attribute(
              name,
              type,
              default: column.default,
              user_provided_default: false
            )
          end
        end
    end
  end
end

之后 ActiveRecord::Attributes 接管定义您通过架构缓存中的设置器和获取器与之交互的实际属性。同样在这里,真正的魔法发生在文档很少的方法中,这是内部 API:

所期望的
module ActiveRecord
  # See ActiveRecord::Attributes::ClassMethods for documentation
  module Attributes
    extend ActiveSupport::Concern

    included do
      class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal:
    end

    module ClassMethods
      # This is the low level API which sits beneath +attribute+. It only
      # accepts type objects, and will do its work immediately instead of
      # waiting for the schema to load. Automatic schema detection and
      # ClassMethods#attribute both call this under the hood. While this method
      # is provided so it can be used by plugin authors, application code
      # should probably use ClassMethods#attribute.
      #
      # +name+ The name of the attribute being defined. Expected to be a +String+.
      #
      # +cast_type+ The type object to use for this attribute.
      #
      # +default+ The default value to use when no value is provided. If this option
      # is not passed, the previous default value (if any) will be used.
      # Otherwise, the default will be +nil+. A proc can also be passed, and
      # will be called once each time a new value is needed.
      #
      # +user_provided_default+ Whether the default value should be cast using
      # +cast+ or +deserialize+.
      def define_attribute(
        name,
        cast_type,
        default: NO_DEFAULT_PROVIDED,
        user_provided_default: true
      )
        attribute_types[name] = cast_type
        define_default_attribute(name, default, cast_type, from_user: user_provided_default)
      end

      def load_schema! # :nodoc:
        super
        attributes_to_define_after_schema_loads.each do |name, (type, options)|
          define_attribute(name, _lookup_cast_type(name, type, options), **options.slice(:default))
        end
      end
    # ...
  end
end

尽管一直存在的神话 schema.rb 与任何方式都没有关系。