attributes.rb 4.9 KB
Newer Older
1
module ActiveRecord
S
Sean Griffin 已提交
2
  module Attributes # :nodoc:
3 4
    extend ActiveSupport::Concern

5
    Type = ActiveRecord::Type
6

7
    included do
S
Sean Griffin 已提交
8
      class_attribute :user_provided_columns, instance_accessor: false # :internal:
9
      class_attribute :user_provided_defaults, instance_accessor: false # :internal:
10
      self.user_provided_columns = {}
11
      self.user_provided_defaults = {}
12 13

      delegate :persistable_attribute_names, to: :class
14 15
    end

16
    module ClassMethods # :nodoc:
17
      # Defines or overrides an attribute on this model. This allows customization of
18 19 20
      # Active Record's type casting behavior, as well as adding support for user defined
      # types.
      #
21 22 23 24 25 26 27 28 29 30 31
      # +name+ The name of the methods to define attribute methods for, and the column which
      # this will persist to.
      #
      # +cast_type+ A type object that contains information about how to type cast the value.
      # See the examples section for more information.
      #
      # ==== Options
      # The options hash accepts the following options:
      #
      # +default+ is the default value that the column should use on a new record.
      #
32 33
      # ==== Examples
      #
A
Anton Cherepanov 已提交
34
      # The type detected by Active Record can be overridden.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
      #
      #   # 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
S
Sean Griffin 已提交
51
      #     attribute :price_in_cents, Type::Integer.new
52 53 54 55 56 57
      #   end
      #
      #   # after
      #   store_listing.price_in_cents # => 10
      #
      # Users may also define their own custom types, as long as they respond to the methods
58
      # defined on the value type. The +type_cast+ method on your type object will be called
59
      # with values both from the database, and from your controllers. See
60
      # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your
61 62 63 64 65 66 67 68 69 70 71 72 73 74
      # type objects inherit from an existing type, or the base value type.
      #
      #   class MoneyType < ActiveRecord::Type::Integer
      #     def type_cast(value)
      #       if value.include?('$')
      #         price_in_dollars = value.gsub(/\$/, '').to_f
      #         price_in_dollars * 100
      #       else
      #         value.to_i
      #       end
      #     end
      #   end
      #
      #   class StoreListing < ActiveRecord::Base
S
Sean Griffin 已提交
75
      #     attribute :price_in_cents, MoneyType.new
76 77 78 79
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
S
Sean Griffin 已提交
80
      def attribute(name, cast_type, options = {})
81
        name = name.to_s
82
        clear_caches_calculated_from_columns
83
        # Assign a new hash to ensure that subclasses do not share a hash
84 85 86 87 88
        self.user_provided_columns = user_provided_columns.merge(name => cast_type)

        if options.key?(:default)
          self.user_provided_defaults = user_provided_defaults.merge(name => options[:default])
        end
89 90 91 92
      end

      # Returns an array of column objects for the table associated with this class.
      def columns
93
        @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
94 95 96 97 98 99 100
      end

      # Returns a hash of column objects for the table associated with this class.
      def columns_hash
        @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
      end

101 102 103 104
      def persistable_attribute_names # :nodoc:
        @persistable_attribute_names ||= connection.schema_cache.columns_hash(table_name).keys
      end

105 106
      def reset_column_information # :nodoc:
        super
107
        clear_caches_calculated_from_columns
108 109 110 111 112
      end

      private

      def add_user_provided_columns(schema_columns)
113
        existing_columns = schema_columns.map do |column|
114 115 116 117 118 119
          new_type = user_provided_columns[column.name]
          if new_type
            column.with_type(new_type)
          else
            column
          end
120 121 122
        end

        existing_column_names = existing_columns.map(&:name)
123 124 125
        new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)|
          connection.new_column(name, nil, type)
        end
126 127

        existing_columns + new_columns
128
      end
129

130
      def clear_caches_calculated_from_columns
131
        @arel_table = nil
132
        @attributes_builder = nil
133
        @column_names = nil
134 135 136
        @column_types = nil
        @columns = nil
        @columns_hash = nil
137
        @content_columns = nil
138
        @default_attributes = nil
139
        @persistable_attribute_names = nil
140
      end
141 142 143 144

      def raw_default_values
        super.merge(user_provided_defaults)
      end
145 146 147
    end
  end
end