For ambiguities, read the Rails documentation properly.
Follow Rails ActiveRecord :: Attributes :: ClassMethods documentation.
When I'm creating a form object recently, I'll get a better understanding of ʻattribute`, which is ambiguous within me.
It was a recognition that ʻattr_accessor` can be typed.
attribute(name, cast_type = Type::Value.new, **options)
Defines an attribute that has a model type. Overrides existing attribute types as needed. This allows you to ** control how the value is converted to and from SQL when assigned to the model **. It also changes the behavior of the value passed to ʻActiveRecord :: Base.where`. This allows you to use domain objects with most of Active Record without resorting to implementation details or monkey patches.
name
: The name of the method that defines the attribute method, and the columns that are retained.
cast_type
: A symbol such as: string
or : integer
, or the type object used for this attribute. See the example below for more information on providing custom type objects.
The following options are available.
default
: The default value to use if no value is given. If this option is not specified, the previous default value (?) Will be used if it exists, otherwise the default value will be nil
.
ʻArray (PostgreSQL only): Indicates that the type is ʻarray
.
range
(PostgreSQL only): Indicates that the type is range
.
Using the cast_type
symbol passes additional options to the type object's constructor.
You can override the types found by ActiveRecord.
# 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
In some cases the default value is passed.
# db/schema.rb
create_table :store_listings, force: true do |t|
t.decimal :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 have to be based on database columns.
# 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
}
If you pass an option 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
The user can also define a custom type as long as it is a method defined in the value type. The deserialize
and cast
methods are called by their own type object, using raw input from the database or controller. See ʻActiveModel :: Type :: Value for the expected API. It is recommended that the type object inherits an existing type or ʻActiveRecord :: Type :: Value
.
class MoneyType < ActiveRecord::Type::Integer
def cast(value)
if !value.kind_of?(Numeric) && value.include?('$')
price_in_dollars 0 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
Read the ʻActiveModel :: Type :: Value documentation for more information on custom types. Read the ʻActiveRecord :: Type.register
documentation for the unique types referenced by symbols.
You can also pass a type object directly instead of a symbol.
When ʻActiveRecord :: Base.whereis called, it uses the type defined by the model class and converts it to a SQL value by calling the
serialize` method on its own type object.
class Money < Struct.new(:amount, :currency)
end
class MoneyType < Type::Value
def initialize(currency_converter:)
@currency_converter = currency_converter
end
# value will be the result of +deserialiez+ 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_bitcoints.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 ʻattributecan also change the behavior of dirty tracking. The
changed?and
changed_in_pace? methods are called from the ʻActiveModel :: Dirty
class. See the methods of the ʻActiveModel :: Type :: Value` class for details.
# File activerecord/lib/active_record/attributes.rb, line 208
def attribute(name, cast_type = Type::Value.new, **options)
name = name.to_s
reload_schema_from_cache
self.attributes_to_define_after_schema_loads =
attributes_to_define_after_schema_loads.merge(
name => [cast_type, options]
)
end
define_attribute( name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true )
This is an API located under the low level ʻattribute. Accepts only type objects and does the work immediately instead of waiting for the schema to load. Both automatic schema discovery and
ClassMethods # attributecall this internally. This method is provided for use by plugin authors, but you probably need to use
ClassMethods # attribute` in your application code.
name
: The name of the attribute to be defined. Expected to be String
.
cast_type
: The type object used for this attribute.
default
: The default value to use if no value is given. If this option is not specified, the previous default value (?) Will be used if it exists, otherwise the default value will be nil
. You can also pass a procedure, which will be called whenever a new value is needed.
ʻUser_provided_default: The default value is cast using the
cast or
deserialize` methods.
# File activerecord/lib/actvie_record/attributes.rb, line 236
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