attributes.rb 9.1 KB
Newer Older
1 2
require 'active_record/attribute/user_provided_default'

3
module ActiveRecord
S
Sean Griffin 已提交
4
  # See ActiveRecord::Attributes::ClassMethods for documentation
S
Sean Griffin 已提交
5
  module Attributes
6 7
    extend ActiveSupport::Concern

S
Sean Griffin 已提交
8
    # :nodoc:
9
    Type = ActiveRecord::Type
10

11
    included do
12 13
      class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
      self.attributes_to_define_after_schema_loads = {}
14 15
    end

S
Sean Griffin 已提交
16 17 18 19 20
    module ClassMethods
      # 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
S
Sean Griffin 已提交
21
      # ActiveRecord::QueryMethods#where. This will let you use
S
Sean Griffin 已提交
22 23 24 25 26
      # 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.
27
      #
28 29 30
      # +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.
31 32
      #
      # ==== Options
33
      #
S
Sean Griffin 已提交
34 35 36 37 38
      # 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+.
39
      #
40 41
      # +array+ (PG only) specifies that the type should be an array (see the
      # examples below).
S
Sean Griffin 已提交
42
      #
43 44
      # +range+ (PG only) specifies that the type should be a range (see the
      # examples below).
45
      #
46 47
      # ==== Examples
      #
A
Anton Cherepanov 已提交
48
      # The type detected by Active Record can be overridden.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
      #
      #   # 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.new(10.1)
      #
      #   class StoreListing < ActiveRecord::Base
65
      #     attribute :price_in_cents, :integer
66 67 68 69 70
      #   end
      #
      #   # after
      #   store_listing.price_in_cents # => 10
      #
71 72 73 74 75 76 77 78 79 80 81 82 83 84
      # A default can also be provided.
      #
      #   create_table :store_listings, force: true do |t|
      #     t.string :my_string, default: "original default"
      #   end
      #
      #   StoreListing.new.my_string # => "original default"
      #
      #   class StoreListing < ActiveRecord::Base
      #     attribute :my_string, :string, default: "new default"
      #   end
      #
      #   StoreListing.new.my_string # => "new default"
      #
S
Sean Griffin 已提交
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
      # Attributes do not need to be backed by a database column.
      #
      #   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
      #     }
      #
      # ==== Creating Custom Types
      #
      # Users may also define their own custom types, as long as they respond
109 110 111 112 113
      # 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 ActiveRecord::Type::Value for the
      # expected API. It is recommended that your type objects inherit from an
      # existing type, or from ActiveRecord::Type::Value
114 115
      #
      #   class MoneyType < ActiveRecord::Type::Integer
S
Sean Griffin 已提交
116
      #     def cast(value)
117 118
      #       if value.include?('$')
      #         price_in_dollars = value.gsub(/\$/, '').to_f
S
Sean Griffin 已提交
119
      #         super(price_in_dollars * 100)
120
      #       else
S
Sean Griffin 已提交
121
      #         super
122 123 124 125
      #       end
      #     end
      #   end
      #
S
Sean Griffin 已提交
126 127 128 129
      #   # config/initializers/types.rb
      #   ActiveRecord::Type.register(:money, MoneyType)
      #
      #   # /app/models/store_listing.rb
130
      #   class StoreListing < ActiveRecord::Base
S
Sean Griffin 已提交
131
      #     attribute :price_in_cents, :money
132 133 134 135
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
S
Sean Griffin 已提交
136 137
      #
      # For more details on creating custom types, see the documentation for
S
Sean Griffin 已提交
138 139 140
      # ActiveRecord::Type::Value. For more details on registering your types
      # to be referenced by a symbol, see ActiveRecord::Type.register. You can
      # also pass a type object directly, in place of a symbol.
S
Sean Griffin 已提交
141 142 143
      #
      # ==== Querying
      #
S
Sean Griffin 已提交
144
      # When ActiveRecord::QueryMethods#where is called, it will
S
Sean Griffin 已提交
145
      # use the type defined by the model class to convert the value to SQL,
146
      # calling +serialize+ on your type object. For example:
S
Sean Griffin 已提交
147 148 149 150 151 152 153 154 155
      #
      #   class Money < Struct.new(:amount, :currency)
      #   end
      #
      #   class MoneyType < Type::Value
      #     def initialize(currency_converter)
      #       @currency_converter = currency_converter
      #     end
      #
156
      #     # value will be the result of +deserialize+ or
157
      #     # +cast+. Assumed to be an instance of +Money+ in
S
Sean Griffin 已提交
158
      #     # this case.
159
      #     def serialize(value)
R
Ryuta Kamizono 已提交
160
      #       value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
S
Sean Griffin 已提交
161 162 163 164
      #       value_in_bitcoins.amount
      #     end
      #   end
      #
S
Sean Griffin 已提交
165 166
      #   ActiveRecord::Type.register(:money, MoneyType)
      #
S
Sean Griffin 已提交
167 168
      #   class Product < ActiveRecord::Base
      #     currency_converter = ConversionRatesFromTheInternet.new
S
Sean Griffin 已提交
169
      #     attribute :price_in_bitcoins, :money, currency_converter
S
Sean Griffin 已提交
170 171 172 173 174 175 176 177 178 179 180 181
      #   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?+
S
Sean Griffin 已提交
182 183
      # will be called from ActiveModel::Dirty. See the documentation for those
      # methods in ActiveRecord::Type::Value for more details.
184
      def attribute(name, cast_type, **options)
185
        name = name.to_s
186
        reload_schema_from_cache
187

188 189 190 191
        self.attributes_to_define_after_schema_loads =
          attributes_to_define_after_schema_loads.merge(
            name => [cast_type, options]
          )
192 193
      end

S
Sean Griffin 已提交
194 195 196
      # 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
S
Sean Griffin 已提交
197 198 199
      # 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.
S
Sean Griffin 已提交
200 201 202
      #
      # +name+ The name of the attribute being defined. Expected to be a +String+.
      #
S
Sean Griffin 已提交
203
      # +cast_type+ The type object to use for this attribute.
S
Sean Griffin 已提交
204 205 206 207 208 209
      #
      # +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+.
      #
      # +user_provided_default+ Whether the default value should be cast using
S
Sean Griffin 已提交
210
      # +cast+ or +deserialize+.
211 212 213 214 215 216 217 218
      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)
219 220
      end

S
Sean Griffin 已提交
221
      def load_schema! # :nodoc:
222
        super
223
        attributes_to_define_after_schema_loads.each do |name, (type, options)|
224
          if type.is_a?(Symbol)
225
            type = ActiveRecord::Type.lookup(type, **options.except(:default))
226 227 228
          end

          define_attribute(name, type, **options.slice(:default))
229
        end
230 231 232 233
      end

      private

234 235
      NO_DEFAULT_PROVIDED = Object.new # :nodoc:
      private_constant :NO_DEFAULT_PROVIDED
236

237 238 239 240
      def define_default_attribute(name, value, type, from_user:)
        if value == NO_DEFAULT_PROVIDED
          default_attribute = _default_attributes[name].with_type(type)
        elsif from_user
241 242 243 244 245
          default_attribute = Attribute::UserProvidedDefault.new(
            name,
            value,
            type,
          )
246 247
        else
          default_attribute = Attribute.from_database(name, value, type)
248
        end
249
        _default_attributes[name] = default_attribute
250
      end
251 252 253
    end
  end
end