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.