attribute_methods.rb 14.0 KB
Newer Older
J
Jeremy Kemper 已提交
1
require 'active_support/core_ext/enumerable'
2
require 'mutex_m'
J
Jeremy Kemper 已提交
3

4
module ActiveRecord
5
  # = Active Record Attribute Methods
6
  module AttributeMethods
7
    extend ActiveSupport::Concern
8
    include ActiveModel::AttributeMethods
9

10
    included do
11
      @attribute_methods_generated = false
12 13 14 15 16 17 18 19 20 21
      include Read
      include Write
      include BeforeTypeCast
      include Query
      include PrimaryKey
      include TimeZoneConversion
      include Dirty
      include Serialization
    end

22
    module ClassMethods
23 24 25 26 27 28 29
      def inherited(child_class) #:nodoc:
        child_class.initialize_generated_modules
        super
      end

      def initialize_generated_modules # :nodoc:
        @generated_attribute_methods = Module.new { extend Mutex_m }
30
        @attribute_methods_generated = false
31 32 33
        include @generated_attribute_methods
      end

34 35
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
36
      def define_attribute_methods # :nodoc:
V
Vipul A M 已提交
37
        # Use a mutex; we don't want two thread simultaneously trying to define
J
Jon Leighton 已提交
38
        # attribute methods.
39
        generated_attribute_methods.synchronize do
40
          return false if @attribute_methods_generated
J
Jon Leighton 已提交
41 42 43 44
          superclass.define_attribute_methods unless self == base_class
          super(column_names)
          @attribute_methods_generated = true
        end
45
        true
46 47
      end

48
      def undefine_attribute_methods # :nodoc:
49 50 51 52
        generated_attribute_methods.synchronize do
          super if @attribute_methods_generated
          @attribute_methods_generated = false
        end
53 54
      end

55 56 57 58 59 60 61 62 63 64 65 66 67 68
      # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
      # \Active \Record method is defined in the model, otherwise +false+.
      #
      #   class Person < ActiveRecord::Base
      #     def save
      #       'already defined by Active Record'
      #     end
      #   end
      #
      #   Person.instance_method_already_implemented?(:save)
      #   # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
      #
      #   Person.instance_method_already_implemented?(:name)
      #   # => false
69
      def instance_method_already_implemented?(method_name)
70
        if dangerous_attribute_method?(method_name)
X
Xavier Noria 已提交
71
          raise DangerousAttributeError, "#{method_name} is defined by Active Record"
72 73
        end

J
Jon Leighton 已提交
74
        if superclass == Base
J
Jon Leighton 已提交
75 76
          super
        else
77 78 79
          # If B < A and A defines its own attribute method, then we don't want to overwrite that.
          defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
          defined && !ActiveRecord::Base.method_defined?(method_name) || super
J
Jon Leighton 已提交
80
        end
81
      end
82

83 84
      # A method name is 'dangerous' if it is already defined by Active Record, but
      # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
85
      def dangerous_attribute_method?(name) # :nodoc:
J
Jon Leighton 已提交
86 87
        method_defined_within?(name, Base)
      end
88

89
      def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
J
Jon Leighton 已提交
90 91 92 93 94 95 96 97 98
        if klass.method_defined?(name) || klass.private_method_defined?(name)
          if sup.method_defined?(name) || sup.private_method_defined?(name)
            klass.instance_method(name).owner != sup.instance_method(name).owner
          else
            true
          end
        else
          false
        end
99
      end
100

101 102 103 104 105 106 107 108 109
      # Returns +true+ if +attribute+ is an attribute method and table exists,
      # +false+ otherwise.
      #
      #   class Person < ActiveRecord::Base
      #   end
      #
      #   Person.attribute_method?('name')   # => true
      #   Person.attribute_method?(:age=)    # => true
      #   Person.attribute_method?(:nothing) # => false
110 111 112 113
      def attribute_method?(attribute)
        super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
      end

114 115 116 117 118 119 120 121
      # Returns an array of column names as strings if it's not an abstract class and
      # table exists. Otherwise it returns an empty array.
      #
      #   class Person < ActiveRecord::Base
      #   end
      #
      #   Person.attribute_names
      #   # => ["id", "created_at", "updated_at", "name", "age"]
122 123 124 125 126 127 128
      def attribute_names
        @attribute_names ||= if !abstract_class? && table_exists?
            column_names
          else
            []
          end
      end
129
    end
J
Joshua Peek 已提交
130

131 132
    # If we haven't generated any methods yet, generate them, then
    # see if we've created the method we're looking for.
133
    def method_missing(method, *args, &block) # :nodoc:
134
      if self.class.define_attribute_methods
135 136 137 138 139
        if respond_to_without_attributes?(method)
          send(method, *args, &block)
        else
          super
        end
140 141
      else
        super
142 143 144
      end
    end

145
    def attribute_missing(match, *args, &block) # :nodoc:
146 147 148 149 150 151 152 153 154 155 156 157 158
      if self.class.columns_hash[match.attr_name]
        ActiveSupport::Deprecation.warn(
          "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
          "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
          "is a column of the table. If this error has happened through normal usage of Active " \
          "Record (rather than through your own code or external libraries), please report it as " \
          "a bug."
        )
      end

      super
    end

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
    # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
    # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
    # which will all return +true+. It also define the attribute methods if they have
    # not been generated.
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.respond_to(:name)    # => true
    #   person.respond_to(:name=)   # => true
    #   person.respond_to(:name?)   # => true
    #   person.respond_to('age')    # => true
    #   person.respond_to('age=')   # => true
    #   person.respond_to('age?')   # => true
    #   person.respond_to(:nothing) # => false
175
    def respond_to?(name, include_private = false)
176
      name = name.to_s
177
      self.class.define_attribute_methods
178 179
      result = super

180
      # If the result is false the answer is false.
181 182 183 184
      return false unless result

      # If the result is true then check for the select case.
      # For queries selecting a subset of columns, return false for unselected columns.
185 186
      # We check defined?(@attributes) not to issue warnings if called on objects that
      # have been allocated but not yet initialized.
187
      if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
188 189 190 191
        return has_attribute?(name)
      end

      return true
192
    end
193

194 195 196 197 198 199 200 201 202
    # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.has_attribute?(:name)    # => true
    #   person.has_attribute?('age')    # => true
    #   person.has_attribute?(:nothing) # => false
203 204 205 206 207
    def has_attribute?(attr_name)
      @attributes.has_key?(attr_name.to_s)
    end

    # Returns an array of names for the attributes available on this object.
208 209 210 211 212 213 214
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.attribute_names
    #   # => ["id", "created_at", "updated_at", "name", "age"]
215 216 217 218 219
    def attribute_names
      @attributes.keys
    end

    # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
220 221 222 223 224 225 226
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.create(name: 'Francesco', age: 22)
    #   person.attributes
    #   # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
227
    def attributes
228 229 230
      attribute_names.each_with_object({}) { |name, attrs|
        attrs[name] = read_attribute(name)
      }
231 232 233 234 235 236 237 238
    end

    # Returns an <tt>#inspect</tt>-like string for the value of the
    # attribute +attr_name+. String attributes are truncated upto 50
    # characters, and Date and Time attributes are returned in the
    # <tt>:db</tt> format. Other attributes return the value of
    # <tt>#inspect</tt> without modification.
    #
239
    #   person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
240 241
    #
    #   person.attribute_for_inspect(:name)
242
    #   # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
243 244
    #
    #   person.attribute_for_inspect(:created_at)
245
    #   # => "\"2012-10-22 00:15:07\""
246 247 248 249 250 251 252 253 254 255 256 257
    def attribute_for_inspect(attr_name)
      value = read_attribute(attr_name)

      if value.is_a?(String) && value.length > 50
        "#{value[0..50]}...".inspect
      elsif value.is_a?(Date) || value.is_a?(Time)
        %("#{value.to_s(:db)}")
      else
        value.inspect
      end
    end

258 259 260
    # Returns +true+ if the specified +attribute+ has been set by the user or by a
    # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
    # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
261
    # Note that it always returns +true+ with boolean attributes.
262
    #
263
    #   class Task < ActiveRecord::Base
264 265
    #   end
    #
266 267 268
    #   person = Task.new(title: '', is_done: false)
    #   person.attribute_present?(:title)   # => false
    #   person.attribute_present?(:is_done) # => true
269
    #   person.name = 'Francesco'
270 271 272
    #   person.is_done = true
    #   person.attribute_present?(:title)   # => true
    #   person.attribute_present?(:is_done) # => true
273 274
    def attribute_present?(attribute)
      value = read_attribute(attribute)
275
      !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
276 277
    end

278 279 280 281 282 283 284 285 286 287 288 289
    # Returns the column object for the named attribute. Returns +nil+ if the
    # named attribute not exists.
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
    #   # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
    #
    #   person.column_for_attribute(:nothing)
    #   # => nil
290
    def column_for_attribute(name)
A
Aaron Patterson 已提交
291
      # FIXME: should this return a null object for columns that don't exist?
292 293 294
      self.class.columns_hash[name.to_s]
    end

295
    # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
296 297 298 299
    # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
    # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
    #
    # Alias for the <tt>read_attribute</tt> method.
300 301
    #
    #   class Person < ActiveRecord::Base
302
    #     belongs_to :organization
303 304
    #   end
    #
305
    #   person = Person.new(name: 'Francesco', age: '22')
306 307
    #   person[:name] # => "Francesco"
    #   person[:age]  # => 22
308 309 310 311
    #
    #   person = Person.select('id').first
    #   person[:name]            # => ActiveModel::MissingAttributeError: missing attribute: name
    #   person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
312
    def [](attr_name)
313
      read_attribute(attr_name) { |n| missing_attribute(n, caller) }
314 315 316
    end

    # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
317 318 319 320 321 322 323 324 325
    # (Alias for the protected <tt>write_attribute</tt> method).
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person[:age] = '22'
    #   person[:age] # => 22
    #   person[:age] # => Fixnum
326 327 328 329
    def []=(attr_name, value)
      write_attribute(attr_name, value)
    end

330
    protected
331

332
    def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
333 334 335 336 337 338
      attribute_names.each do |name|
        attributes[name] = clone_attribute_value(reader_method, name)
      end
      attributes
    end

339
    def clone_attribute_value(reader_method, attribute_name) # :nodoc:
340 341 342 343 344 345
      value = send(reader_method, attribute_name)
      value.duplicable? ? value.clone : value
    rescue TypeError, NoMethodError
      value
    end

346
    def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
J
Jon Leighton 已提交
347
      arel_attributes_with_values(attributes_for_create(attribute_names))
R
Robin Roestenburg 已提交
348
    end
349

350
    def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
R
Robin Roestenburg 已提交
351
      arel_attributes_with_values(attributes_for_update(attribute_names))
352 353
    end

354
    def attribute_method?(attr_name) # :nodoc:
355
      # We check defined? because Syck calls respond_to? before actually calling initialize.
356
      defined?(@attributes) && @attributes.include?(attr_name)
357 358
    end

R
Robin Roestenburg 已提交
359
    private
360

R
Robin Roestenburg 已提交
361
    # Returns a Hash of the Arel::Attributes and attribute values that have been
362
    # typecasted for use in an Arel insert/update method.
R
Robin Roestenburg 已提交
363 364 365
    def arel_attributes_with_values(attribute_names)
      attrs = {}
      arel_table = self.class.arel_table
366

R
Robin Roestenburg 已提交
367 368 369
      attribute_names.each do |name|
        attrs[arel_table[name]] = typecasted_attribute_value(name)
      end
370 371 372
      attrs
    end

R
Robin Roestenburg 已提交
373 374 375
    # Filters the primary keys and readonly attributes from the attribute names.
    def attributes_for_update(attribute_names)
      attribute_names.select do |name|
376
        column_for_attribute(name) && !readonly_attribute?(name)
R
Robin Roestenburg 已提交
377 378 379 380 381
      end
    end

    # Filters out the primary keys, from the attribute names, when the primary
    # key is to be generated (e.g. the id attribute has no value).
J
Jon Leighton 已提交
382 383 384
    def attributes_for_create(attribute_names)
      attribute_names.select do |name|
        column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
R
Robin Roestenburg 已提交
385
      end
386 387 388 389 390 391
    end

    def readonly_attribute?(name)
      self.class.readonly_attributes.include?(name)
    end

R
Robin Roestenburg 已提交
392 393
    def pk_attribute?(name)
      column_for_attribute(name).primary
394 395 396
    end

    def typecasted_attribute_value(name)
397 398 399 400
      # FIXME: we need @attributes to be used consistently.
      # If the values stored in @attributes were already typecasted, this code
      # could be simplified
      read_attribute(name)
401
    end
402 403
  end
end