Home > OS >  Active Record Model Default Mapping
Active Record Model Default Mapping

Time:09-23

I am fairly new to Rails, and one thing I don't understand is where in the rails official documentation it says that the database defaults map to the models.

For example, if I put a default value on a column in a migration, I would expect that the default value is inserted for a particular record when it is saved to the database. However I have noticed that when I do Record.new the model attributes already have those defaults that were set in the database! This is useful because it means I don't have to explicitly set it when instantiating a new model object, but where in the docs does it say this automatic setting of default on new objects happens?

CodePudding user response:

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

It doesn't. The low level implementation of how ActiveRecord does the magic of reading your database schema and going from there to defining defaults when instanciate a a new record is spread across multiple APIs - some of them internal. If you want to know how it works in detail you need to dig through the code. But you don't really need to know it to write Rails applications.

What you really need to know is that ActiveRecord reads the schema from the database via the database adapter when the class is first evaluated. This schema information is cached on the class so that AR doesn't have to query the DB again and contains information about the type and default values of the database columns.

This information is then used to define a column cache on the model and attributes which are a very diffuse term for the metadata stored about a attribute, its values before and after type casting and the setters and getters you use to access them. Don't get fooled that this in any way behaves like a simple instance variable - your foo attribute is not stored in @foo.

ActiveRecord/ActiveModel knows to set the defaults when instanciating a model since it looks at the attributes of the model.

ActiveRecord::ModelSchema which is an internal API is largely responsible mapping the DB schema to the column cache on the model:

# 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

After that ActiveRecord::Attributes takes over defines the actual attributes that you interact with via setters and getters from the schema cache. Also here the real magic is happening in methods with very little documentation which is to be expected for an internal 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

Depite the persistent myth schema.rb is not involved in any way.

  • Related