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

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

9 10 11 12 13 14 15 16 17 18 19
    included do
      include Read
      include Write
      include BeforeTypeCast
      include Query
      include PrimaryKey
      include TimeZoneConversion
      include Dirty
      include Serialization
    end

20
    module ClassMethods
21 22
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
23
      def define_attribute_methods
J
Jon Leighton 已提交
24 25 26 27 28 29 30 31
        # Use a mutex; we don't want two thread simaltaneously trying to define
        # attribute methods.
        @attribute_methods_mutex.synchronize do
          return if attribute_methods_generated?
          superclass.define_attribute_methods unless self == base_class
          super(column_names)
          @attribute_methods_generated = true
        end
32 33 34
      end

      def attribute_methods_generated?
J
Jon Leighton 已提交
35
        @attribute_methods_generated ||= false
36 37
      end

J
Jon Leighton 已提交
38
      def undefine_attribute_methods
39
        super if attribute_methods_generated?
J
Jon Leighton 已提交
40
        @attribute_methods_generated = false
41 42
      end

43
      def instance_method_already_implemented?(method_name)
44 45 46 47
        if dangerous_attribute_method?(method_name)
          raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
        end

48
        if [Base, Model].include?(active_record_super)
J
Jon Leighton 已提交
49 50
          super
        else
51 52 53
          # 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 已提交
54
        end
55
      end
56

57 58
      # 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.)
J
Jon Leighton 已提交
59 60 61
      def dangerous_attribute_method?(name)
        method_defined_within?(name, Base)
      end
62

J
Jon Leighton 已提交
63 64 65 66 67 68 69 70 71 72
      def method_defined_within?(name, klass, sup = klass.superclass)
        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
73
      end
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

      def attribute_method?(attribute)
        super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
      end

      # Returns an array of column names as strings if it's not
      # an abstract class and table exists.
      # Otherwise it returns an empty array.
      def attribute_names
        @attribute_names ||= if !abstract_class? && table_exists?
            column_names
          else
            []
          end
      end
89
    end
J
Joshua Peek 已提交
90

91 92 93 94
    # If we haven't generated any methods yet, generate them, then
    # see if we've created the method we're looking for.
    def method_missing(method, *args, &block)
      unless self.class.attribute_methods_generated?
95
        self.class.define_attribute_methods
96 97 98 99 100 101

        if respond_to_without_attributes?(method)
          send(method, *args, &block)
        else
          super
        end
102 103
      else
        super
104 105 106
      end
    end

107 108 109 110 111 112 113 114 115 116 117 118 119 120
    def attribute_missing(match, *args, &block)
      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

121
    def respond_to?(name, include_private = false)
122
      self.class.define_attribute_methods unless self.class.attribute_methods_generated?
123 124
      super
    end
125

126 127 128 129 130 131 132 133 134 135 136 137
    # Returns true if the given attribute is in the attributes hash
    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.
    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.
    def attributes
138 139 140
      attribute_names.each_with_object({}) { |name, attrs|
        attrs[name] = read_attribute(name)
      }
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    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.
    #
    #   person = Person.create!(:name => "David Heinemeier Hansson " * 3)
    #
    #   person.attribute_for_inspect(:name)
    #   # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
    #
    #   person.attribute_for_inspect(:created_at)
    #   # => '"2009-01-12 04:48:57"'
    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

    # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
    # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
    def attribute_present?(attribute)
      value = read_attribute(attribute)
172
      !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
173 174 175 176
    end

    # Returns the column object for the named attribute.
    def column_for_attribute(name)
A
Aaron Patterson 已提交
177
      # FIXME: should this return a null object for columns that don't exist?
178 179 180
      self.class.columns_hash[name.to_s]
    end

181 182 183 184 185 186 187 188 189 190 191 192 193
    # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
    # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
    # (Alias for the protected read_attribute method).
    def [](attr_name)
      read_attribute(attr_name)
    end

    # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
    # (Alias for the protected write_attribute method).
    def []=(attr_name, value)
      write_attribute(attr_name, value)
    end

194
    protected
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209

    def clone_attributes(reader_method = :read_attribute, attributes = {})
      attribute_names.each do |name|
        attributes[name] = clone_attribute_value(reader_method, name)
      end
      attributes
    end

    def clone_attribute_value(reader_method, attribute_name)
      value = send(reader_method, attribute_name)
      value.duplicable? ? value.clone : value
    rescue TypeError, NoMethodError
      value
    end

R
Robin Roestenburg 已提交
210 211 212
    def arel_attributes_with_values_for_create(pk_attribute_allowed)
      arel_attributes_with_values(attributes_for_create(pk_attribute_allowed))
    end
213

R
Robin Roestenburg 已提交
214 215
    def arel_attributes_with_values_for_update(attribute_names)
      arel_attributes_with_values(attributes_for_update(attribute_names))
216 217 218
    end

    def attribute_method?(attr_name)
219
      defined?(@attributes) && @attributes.include?(attr_name)
220 221
    end

R
Robin Roestenburg 已提交
222
    private
223

R
Robin Roestenburg 已提交
224 225 226 227 228
    # Returns a Hash of the Arel::Attributes and attribute values that have been
    # type casted for use in an Arel insert/update method.
    def arel_attributes_with_values(attribute_names)
      attrs = {}
      arel_table = self.class.arel_table
229

R
Robin Roestenburg 已提交
230 231 232
      attribute_names.each do |name|
        attrs[arel_table[name]] = typecasted_attribute_value(name)
      end
233 234 235
      attrs
    end

R
Robin Roestenburg 已提交
236 237 238 239 240 241 242 243 244 245 246 247 248
    # Filters the primary keys and readonly attributes from the attribute names.
    def attributes_for_update(attribute_names)
      attribute_names.select do |name|
        column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name)
      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).
    def attributes_for_create(pk_attribute_allowed)
      @attributes.keys.select do |name|
        column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name))
      end
249 250 251 252 253 254
    end

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

R
Robin Roestenburg 已提交
255 256
    def pk_attribute?(name)
      column_for_attribute(name).primary
257 258 259 260 261 262 263 264 265 266 267
    end

    def typecasted_attribute_value(name)
      if self.class.serialized_attributes.include?(name)
        @attributes[name].serialized_value
      else
        # 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)
      end
268
    end
269 270
  end
end