Module: ActiveRecord::Attributes::ClassMethods
Relationships & Source Files | |
Defined in: | activerecord/lib/active_record/attributes.rb |
Instance Method Summary
-
#attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
Defines an attribute with a type on this model.
-
#define_attribute(name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true)
This is the low level API which sits beneath #attribute.
Instance Method Details
#attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
Defines an attribute with a type on this model. It will override the type of existing attributes if needed. This allows control over how values are converted to and from SQL when assigned to a model. It also changes the behavior of values passed to {ActiveRecord::Base.where}. This will let you use your domain objects across much of Active Record, without having to rely on implementation details or monkey patching.
name
The name of the methods to define attribute methods for, and the column which this will persist to.
cast_type
A symbol such as :string
or :integer
, or a type object to be used for this attribute. See the examples below for more information about providing custom type objects.
Options
The following options are accepted:
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
.
array
(PostgreSQL only) specifies that the type should be an array (see the examples below).
range
(PostgreSQL only) specifies that the type should be a range (see the examples below).
When using a symbol for cast_type
, extra options are forwarded to the constructor of the type object.
Examples
The type detected by Active Record can be overridden.
# db/schema.rb
create_table :store_listings, force: true do |t|
t.decimal :price_in_cents
end
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end
store_listing = StoreListing.new(price_in_cents: '10.1')
# before
store_listing.price_in_cents # => BigDecimal(10.1)
class StoreListing < ActiveRecord::Base
attribute :price_in_cents, :integer
end
# after
store_listing.price_in_cents # => 10
A default can also be provided.
# db/schema.rb
create_table :store_listings, force: true do |t|
t.string :my_string, default: "original default"
end
StoreListing.new.my_string # => "original default"
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
attribute :my_string, :string, default: "new default"
end
StoreListing.new.my_string # => "new default"
class Product < ActiveRecord::Base
attribute :my_default_proc, :datetime, default: -> { Time.now }
end
Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
Attributes do not need to be backed by a database column.
# app/models/my_model.rb
class MyModel < ActiveRecord::Base
attribute :my_string, :string
attribute :my_int_array, :integer, array: true
attribute :my_float_range, :float, range: true
end
model = MyModel.new(
my_string: "string",
my_int_array: ["1", "2", "3"],
my_float_range: "[1,3.5]",
)
model.attributes
# =>
{
my_string: "string",
my_int_array: [1, 2, 3],
my_float_range: 1.0..3.5
}
Passing options to the type constructor
# app/models/my_model.rb
class MyModel < ActiveRecord::Base
attribute :small_int, :integer, limit: 2
end
MyModel.create(small_int: 65537)
# => Error: 65537 is out of range for the limit of two bytes
Creating Custom Types
Users may also define their own custom types, as long as they respond to the methods defined on the value type. The method deserialize
or cast
will be called on your type object, with raw input from the database or from your controllers. See ::ActiveModel::Type::Value
for the expected API. It is recommended that your type objects inherit from an existing type, or from Type::Value
class MoneyType < ActiveRecord::Type::Integer
def cast(value)
if !value.kind_of?(Numeric) && value.include?('$')
price_in_dollars = value.gsub(/\$/, '').to_f
super(price_in_dollars * 100)
else
super
end
end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
attribute :price_in_cents, :money
end
store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000
For more details on creating custom types, see the documentation for ::ActiveModel::Type::Value
. For more details on registering your types to be referenced by a symbol, see Type.register. You can also pass a type object directly, in place of a symbol.
Querying
When {ActiveRecord::Base.where} is called, it will use the type defined by the model class to convert the value to SQL, calling serialize
on your type object. For example:
class Money < Struct.new(:amount, :currency)
end
class MoneyType < ActiveRecord::Type::Value
def initialize(currency_converter:)
@currency_converter = currency_converter
end
# value will be the result of {deserialize} or
# {cast}. Assumed to be an instance of {Money} in
# this case.
def serialize(value)
value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
value_in_bitcoins.amount
end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/product.rb
class Product < ActiveRecord::Base
currency_converter = ConversionRatesFromTheInternet.new
attribute :price_in_bitcoins, :money, currency_converter: currency_converter
end
Product.where(price_in_bitcoins: Money.new(5, "USD"))
# SELECT * FROM products WHERE price_in_bitcoins = 0.02230
Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# SELECT * FROM products WHERE price_in_bitcoins = 0.03412
Dirty Tracking
The type of an attribute is given the opportunity to change how dirty tracking is performed. The methods changed?
and changed_in_place?
will be called from ::ActiveModel::Dirty
. See the documentation for those methods in ::ActiveModel::Type::Value
for more details.
# File 'activerecord/lib/active_record/attributes.rb', line 208
def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, ** ) name = name.to_s name = attribute_aliases[name] || name reload_schema_from_cache case cast_type when Symbol cast_type = Type.lookup(cast_type, **, adapter: Type.adapter_name_from(self)) when nil if (prev_cast_type, prev_default = attributes_to_define_after_schema_loads[name]) default = prev_default if default == NO_DEFAULT_PROVIDED else prev_cast_type = -> subtype { subtype } end cast_type = if block_given? #=> subtype { yield Proc === prev_cast_type ? prev_cast_type[subtype] : prev_cast_type } else prev_cast_type end end self.attributes_to_define_after_schema_loads = attributes_to_define_after_schema_loads.merge(name => [cast_type, default]) end
#define_attribute(name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true)
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 #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 #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
.
# File 'activerecord/lib/active_record/attributes.rb', line 253
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