diff options
Diffstat (limited to 'activerecord/lib/active_record/attribute_methods')
8 files changed, 178 insertions, 128 deletions
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index bde11d0494..d4f529acbf 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -20,11 +20,7 @@ module ActiveRecord # Handle *_before_type_cast for method_missing. def attribute_before_type_cast(attribute_name) - if attribute_name == 'id' - read_attribute_before_type_cast(self.class.primary_key) - else - read_attribute_before_type_cast(attribute_name) - end + read_attribute_before_type_cast(attribute_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 40e4a97e73..3a737e5b35 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -55,12 +55,10 @@ module ActiveRecord # The attribute already has an unsaved change. if attribute_changed?(attr) old = @changed_attributes[attr] - @changed_attributes.delete(attr) unless field_changed?(attr, old, value) + @changed_attributes.delete(attr) unless _field_changed?(attr, old, value) else old = clone_attribute_value(:read_attribute, attr) - # Save Time objects as TimeWithZone if time_zone_aware_attributes == true - old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old) - @changed_attributes[attr] = old if field_changed?(attr, old, value) + @changed_attributes[attr] = old if _field_changed?(attr, old, value) end # Carry on. @@ -77,7 +75,7 @@ module ActiveRecord end end - def field_changed?(attr, old, value) + def _field_changed?(attr, old, value) if column = column_for_attribute(attr) if column.number? && column.null && (old.nil? || old == 0) && value.blank? # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. @@ -92,10 +90,6 @@ module ActiveRecord old != value end - - def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym) - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index a7785f8786..8e0b3e1402 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -1,3 +1,5 @@ +require 'set' + module ActiveRecord module AttributeMethods module PrimaryKey @@ -24,6 +26,17 @@ module ActiveRecord query_attribute(self.class.primary_key) end + # Returns the primary key value before type cast + def id_before_type_cast + read_attribute_before_type_cast(self.class.primary_key) + end + + protected + + def attribute_method?(attr_name) + attr_name == 'id' || super + end + module ClassMethods def define_method_attribute(attr_name) super @@ -39,12 +52,15 @@ module ActiveRecord end end + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + def dangerous_attribute_method?(method_name) - super && !['id', 'id=', 'id?'].include?(method_name) + super && !ID_ATTRIBUTE_METHODS.include?(method_name) end # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the - # primary_key_prefix_type setting, though. + # primary_key_prefix_type setting, though. Since primary keys are usually protected from mass assignment, + # remember to let your database generate them or include the key in +attr_accessible+. def primary_key @primary_key = reset_primary_key unless defined? @primary_key @primary_key diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 948809c65a..1e841dc8e0 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -10,8 +10,11 @@ module ActiveRecord end def query_attribute(attr_name) - unless value = read_attribute(attr_name) - false + value = read_attribute(attr_name) + + case value + when true then true + when false, nil then false else column = self.class.columns_hash[attr_name] if column.nil? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 964c4123ef..846ac03d82 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -58,78 +58,88 @@ module ActiveRecord end protected - # We want to generate the methods via module_eval rather than define_method, - # because define_method is slower on dispatch and uses more memory (because it - # creates a closure). - # - # But sometimes the database might return columns with characters that are not - # allowed in normal method names (like 'my_column(omg)'. So to work around this - # we first define with the __temp__ identifier, and then use alias method to - # rename it to what we want. - def define_method_attribute(attr_name) - cast_code = attribute_cast_code(attr_name) - - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__ - #{internal_attribute_access_code(attr_name, cast_code)} - end - alias_method '#{attr_name}', :__temp__ - undef_method :__temp__ - STR - - generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__(v, attributes, attributes_cache, attr_name) - #{external_attribute_access_code(attr_name, cast_code)} - end - alias_method '#{attr_name}', :__temp__ - undef_method :__temp__ - STR - end - - private - def cacheable_column?(column) - attribute_types_cached_by_default.include?(column.type) - end - - def internal_attribute_access_code(attr_name, cast_code) - access_code = "(v=@attributes[attr_name]) && #{cast_code}" - - unless attr_name == primary_key - access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ") + # We want to generate the methods via module_eval rather than define_method, + # because define_method is slower on dispatch and uses more memory (because it + # creates a closure). + # + # But sometimes the database might return columns with characters that are not + # allowed in normal method names (like 'my_column(omg)'. So to work around this + # we first define with the __temp__ identifier, and then use alias method to + # rename it to what we want. + def define_method_attribute(attr_name) + cast_code = attribute_cast_code(attr_name) + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__ + #{internal_attribute_access_code(attr_name, cast_code)} end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR - if cache_attribute?(attr_name) - access_code = "@attributes_cache[attr_name] ||= (#{access_code})" + generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__(v, attributes, attributes_cache, attr_name) + #{external_attribute_access_code(attr_name, cast_code)} end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR + end - "attr_name = '#{attr_name}'; #{access_code}" + private + def cacheable_column?(column) + if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + ! serialized_attributes.include? column.name + else + attribute_types_cached_by_default.include?(column.type) end + end - def external_attribute_access_code(attr_name, cast_code) - access_code = "v && #{cast_code}" + def internal_attribute_access_code(attr_name, cast_code) + "read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) }" + end - if cache_attribute?(attr_name) - access_code = "attributes_cache[attr_name] ||= (#{access_code})" - end + def external_attribute_access_code(attr_name, cast_code) + access_code = "v && #{cast_code}" - access_code + if cache_attribute?(attr_name) + access_code = "attributes_cache[attr_name] ||= (#{access_code})" end - def attribute_cast_code(attr_name) - columns_hash[attr_name].type_cast_code('v') - end + access_code + end + + def attribute_cast_code(attr_name) + columns_hash[attr_name].type_cast_code('v') + end end # 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)). def read_attribute(attr_name) - self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache) + # If it's cached, just return it + @attributes_cache.fetch(attr_name) { |name| + column = @columns_hash.fetch(name) { + return self.class.type_cast_attribute(name, @attributes, @attributes_cache) + } + + value = @attributes.fetch(name) { + return block_given? ? yield(name) : nil + } + + if self.class.cache_attribute?(name) + @attributes_cache[name] = column.type_cast(value) + else + column.type_cast value + end + } end private - def attribute(attribute_name) - read_attribute(attribute_name) - end + + def attribute(attribute_name) + read_attribute(attribute_name) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 0c8e4e4b9a..165785c8fb 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -10,6 +10,20 @@ module ActiveRecord self.serialized_attributes = {} end + class Type # :nodoc: + def initialize(column) + @column = column + end + + def type_cast(value) + value.unserialized_value + end + + def type + @column.type + end + end + class Attribute < Struct.new(:coder, :value, :state) def unserialized_value state == :serialized ? unserialize : value @@ -88,6 +102,14 @@ module ActiveRecord super end end + + def read_attribute_before_type_cast(attr_name) + if serialized_attributes.include?(attr_name) + super.unserialized_value + else + super + end + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 2f86e32f41..20372c5c18 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -4,6 +4,21 @@ require 'active_support/core_ext/object/inclusion' module ActiveRecord module AttributeMethods module TimeZoneConversion + class Type # :nodoc: + def initialize(column) + @column = column + end + + def type_cast(value) + value = @column.type_cast(value) + value.acts_like?(:time) ? value.in_time_zone : value + end + + def type + @column.type + end + end + extend ActiveSupport::Concern included do @@ -16,46 +31,48 @@ module ActiveRecord module ClassMethods protected - # The enhanced read method automatically converts the UTC time stored in the database to the time - # zone stored in Time.zone. - def attribute_cast_code(attr_name) - column = columns_hash[attr_name] - - if create_time_zone_conversion_attribute?(attr_name, column) - typecast = "v = #{super}" - time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" - - "((#{typecast}) && (#{time_zone_conversion}))" - else - super - end + # The enhanced read method automatically converts the UTC time stored in the database to the time + # zone stored in Time.zone. + def attribute_cast_code(attr_name) + column = columns_hash[attr_name] + + if create_time_zone_conversion_attribute?(attr_name, column) + typecast = "v = #{super}" + time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" + + "((#{typecast}) && (#{time_zone_conversion}))" + else + super end + end - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(original_time) - time = original_time - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, original_time) - @attributes_cache["#{attr_name}"] = time + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. + def define_method_attribute=(attr_name) + if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + method_body, line = <<-EOV, __LINE__ + 1 + def #{attr_name}=(original_time) + time = original_time + unless time.acts_like?(:time) + time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super - end + time = time.in_time_zone rescue nil if time + write_attribute(:#{attr_name}, original_time) + @attributes_cache["#{attr_name}"] = time + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, line) + else + super end + end private - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp]) - end + def create_time_zone_conversion_attribute?(name, column) + time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && + [:datetime, :timestamp].include?(column.type) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index fde55b95da..50435921b1 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -28,6 +28,12 @@ module ActiveRecord @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) + # If we're dealing with a binary column, write the data to the cache + # so we don't attempt to typecast multiple times. + if column && column.binary? + @attributes_cache[attr_name] = value + end + if column || @attributes.has_key?(attr_name) @attributes[attr_name] = type_cast_attribute_for_write(column, value) else @@ -37,30 +43,16 @@ module ActiveRecord alias_method :raw_write_attribute, :write_attribute private - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) - end + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end - def type_cast_attribute_for_write(column, value) - if column && column.number? - convert_number_column_value(value) - else - value - end - end + def type_cast_attribute_for_write(column, value) + return value unless column - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end + column.type_cast_for_write value + end end end end |