首页 > 解决方案 > Active Record 模型默认映射

问题描述

我对 Rails 相当陌生,我不明白的一件事是在 rails 官方文档中它说数据库默认映射到模型的位置。

例如,如果我在迁移中将默认值放在列上,我希望在将特定记录保存到数据库时插入默认值。但是我注意到,当我这样做时Record.new,模型属性已经具有在数据库中设置的那些默认值!这很有用,因为这意味着我在实例化新模型对象时不必显式设置它,但是在文档中的什么地方说这种新对象的默认自动设置发生了?

标签: ruby-on-railsmigration

解决方案


但是在文档中的哪个地方说这种新对象的默认自动设置发生了?

它没有。ActiveRecord 如何在实例化新记录时执行读取数据库模式并从那里定义默认值的魔力的低级实现分布在多个 API 中——其中一些是内部的。如果您想详细了解它的工作原理,则需要深入研究代码。但是你并不需要真正了解它来编写 Rails 应用程序。

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

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

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

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

# 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接管定义了您通过模式缓存中的 setter 和 getter 与之交互的实际属性。同样在这里,真正的魔力发生在文档很少的方法中,这是内部 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没有以任何方式参与。


推荐阅读