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

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

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
    included do
      include Read
      include Write
      include BeforeTypeCast
      include Query
      include PrimaryKey
      include TimeZoneConversion
      include Dirty
      include Serialization

      # 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).
      alias [] read_attribute

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

      public :[], :[]=
    end

32
    module ClassMethods
33 34
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
35
      def define_attribute_methods
36
        return if attribute_methods_generated?
J
Jon Leighton 已提交
37 38 39
        superclass.define_attribute_methods unless self == base_class
        super(column_names)
        @attribute_methods_generated = true
40 41 42
      end

      def attribute_methods_generated?
J
Jon Leighton 已提交
43
        @attribute_methods_generated ||= false
44 45
      end

J
Jon Leighton 已提交
46 47 48 49
      # We will define the methods as instance methods, but will call them as singleton
      # methods. This allows us to use method_defined? to check if the method exists,
      # which is fast and won't give any false positives from the ancestors (because
      # there are no ancestors).
50
      def generated_external_attribute_methods
J
Jon Leighton 已提交
51
        @generated_external_attribute_methods ||= Module.new { extend self }
52 53
      end

J
Jon Leighton 已提交
54
      def undefine_attribute_methods
J
Jon Leighton 已提交
55 56
        super
        @attribute_methods_generated = false
57 58
      end

59
      def instance_method_already_implemented?(method_name)
60 61 62 63
        if dangerous_attribute_method?(method_name)
          raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
        end

J
Jon Leighton 已提交
64 65 66 67 68
        if superclass == Base
          super
        else
          method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) || super
        end
69
      end
70

71 72
      # 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 已提交
73 74 75
      def dangerous_attribute_method?(name)
        method_defined_within?(name, Base)
      end
76

J
Jon Leighton 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89
      # Note that we could do this via klass.instance_methods(false), but this would require us
      # to maintain a cached Set (for speed) and invalidate it at the correct time, which would
      # be a pain. This implementation is also O(1) while avoiding maintaining a cached Set.
      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
90
      end
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105

      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
106
    end
J
Joshua Peek 已提交
107

108 109 110 111
    # 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?
112
        self.class.define_attribute_methods
113 114 115 116 117 118

        if respond_to_without_attributes?(method)
          send(method, *args, &block)
        else
          super
        end
119 120
      else
        super
121 122 123
      end
    end

124 125 126 127 128 129 130 131 132 133 134 135 136 137
    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

138
    def respond_to?(name, include_private = false)
139
      self.class.define_attribute_methods unless self.class.attribute_methods_generated?
140 141
      super
    end
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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
    # 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
      Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
    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)
      !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
    end

    # Returns the column object for the named attribute.
    def column_for_attribute(name)
      self.class.columns_hash[name.to_s]
    end

195
    protected
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234

    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

    # Returns a copy of the attributes hash where all the values have been safely quoted for use in
    # an Arel insert/update method.
    def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
      attrs      = {}
      klass      = self.class
      arel_table = klass.arel_table

      attribute_names.each do |name|
        if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)

          if include_readonly_attributes || !self.class.readonly_attributes.include?(name)

            value = if klass.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 type
                      # casted, this code could be simplified
                      read_attribute(name)
                    end

            attrs[arel_table[name]] = value
          end
        end
235
      end
236 237 238 239 240 241 242

      attrs
    end

    def attribute_method?(attr_name)
      attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
    end
243 244
  end
end