Permalink
Browse files

Refactoring attributes/types [#3348 state:resolved]

Signed-off-by: Joshua Peek <josh@joshpeek.com>
  • Loading branch information...
1 parent e13d232 commit f936a1f100e75082081e782e5cceb272885c2df7 @eac eac committed with josh Oct 17, 2009
Showing with 760 additions and 148 deletions.
  1. +16 −0 activerecord/lib/active_record.rb
  2. +3 −10 activerecord/lib/active_record/attribute_methods/before_type_cast.rb
  3. +3 −17 activerecord/lib/active_record/attribute_methods/query.rb
  4. +4 −45 activerecord/lib/active_record/attribute_methods/read.rb
  5. +10 −38 activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
  6. +2 −7 activerecord/lib/active_record/attribute_methods/write.rb
  7. +37 −0 activerecord/lib/active_record/attributes.rb
  8. +42 −0 activerecord/lib/active_record/attributes/aliasing.rb
  9. +15 −0 activerecord/lib/active_record/attributes/store.rb
  10. +111 −0 activerecord/lib/active_record/attributes/typecasting.rb
  11. +7 −31 activerecord/lib/active_record/base.rb
  12. +38 −0 activerecord/lib/active_record/types.rb
  13. +30 −0 activerecord/lib/active_record/types/number.rb
  14. +37 −0 activerecord/lib/active_record/types/object.rb
  15. +33 −0 activerecord/lib/active_record/types/serialize.rb
  16. +20 −0 activerecord/lib/active_record/types/time_with_zone.rb
  17. +37 −0 activerecord/lib/active_record/types/unknown.rb
  18. +20 −0 activerecord/test/cases/attributes/aliasing_test.rb
  19. +118 −0 activerecord/test/cases/attributes/typecasting_test.rb
  20. +30 −0 activerecord/test/cases/types/number_test.rb
  21. +24 −0 activerecord/test/cases/types/object_test.rb
  22. +20 −0 activerecord/test/cases/types/serialize_test.rb
  23. +42 −0 activerecord/test/cases/types/time_with_zone_test.rb
  24. +29 −0 activerecord/test/cases/types/unknown_test.rb
  25. +32 −0 activerecord/test/cases/types_test.rb
@@ -51,6 +51,7 @@ def self.load_all!
autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods'
+ autoload :Attributes, 'active_record/attributes'
autoload :AutosaveAssociation, 'active_record/autosave_association'
autoload :Relation, 'active_record/relation'
autoload :Base, 'active_record/base'
@@ -74,6 +75,7 @@ def self.load_all!
autoload :TestCase, 'active_record/test_case'
autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions'
+ autoload :Types, 'active_record/types'
autoload :Validator, 'active_record/validator'
autoload :Validations, 'active_record/validations'
@@ -87,6 +89,20 @@ module AttributeMethods
autoload :Write, 'active_record/attribute_methods/write'
end
+ module Attributes
+ autoload :Aliasing, 'active_record/attributes/aliasing'
+ autoload :Store, 'active_record/attributes/store'
+ autoload :Typecasting, 'active_record/attributes/typecasting'
+ end
+
+ module Type
+ autoload :Number, 'active_record/types/number'
+ autoload :Object, 'active_record/types/object'
+ autoload :Serialize, 'active_record/types/serialize'
+ autoload :TimeWithZone, 'active_record/types/time_with_zone'
+ autoload :Unknown, 'active_record/types/unknown'
+ end
+
module Locking
autoload :Optimistic, 'active_record/locking/optimistic'
autoload :Pessimistic, 'active_record/locking/pessimistic'
@@ -8,25 +8,18 @@ module BeforeTypeCast
end
def read_attribute_before_type_cast(attr_name)
- @attributes[attr_name]
+ _attributes.without_typecast[attr_name]
end
# Returns a hash of attributes before typecasting and deserialization.
def attributes_before_type_cast
- self.attribute_names.inject({}) do |attrs, name|
- attrs[name] = read_attribute_before_type_cast(name)
- attrs
- end
+ _attributes.without_typecast
end
private
# 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
@@ -8,23 +8,7 @@ module Query
end
def query_attribute(attr_name)
- unless value = read_attribute(attr_name)
- false
- else
- column = self.class.columns_hash[attr_name]
- if column.nil?
- if Numeric === value || value !~ /[^0-9]/
- !value.to_i.zero?
- else
- return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
- !value.blank?
- end
- elsif column.number?
- !value.zero?
- else
- !value.blank?
- end
- end
+ _attributes.has?(attr_name)
end
private
@@ -35,3 +19,5 @@ def attribute?(attribute_name)
end
end
end
+
+
@@ -37,30 +37,20 @@ def cache_attribute?(attr_name)
protected
def define_method_attribute(attr_name)
- if self.serialized_attributes[attr_name]
- define_read_method_for_serialized_attribute(attr_name)
- else
- define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
- end
+ define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
if attr_name == primary_key && attr_name != "id"
define_read_method(:id, attr_name, columns_hash[attr_name])
end
end
private
- # Define read method for serialized attribute.
- def define_read_method_for_serialized_attribute(attr_name)
- generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
- end
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
- cast_code = column.type_cast_code('v') if column
- access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
-
+ access_code = "_attributes['#{attr_name}']"
unless attr_name.to_s == self.primary_key.to_s
- access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+ access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless _attributes.key?('#{attr_name}'); ")
end
if cache_attribute?(attr_name)
@@ -73,38 +63,7 @@ def define_read_method(symbol, attr_name, column)
# 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)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id'
- if !(value = @attributes[attr_name]).nil?
- if column = column_for_attribute(attr_name)
- if unserializable_attribute?(attr_name, column)
- unserialize_attribute(attr_name)
- else
- column.type_cast(value)
- end
- else
- value
- end
- else
- nil
- end
- end
-
- # Returns true if the attribute is of a text column and marked for serialization.
- def unserializable_attribute?(attr_name, column)
- column.text? && self.class.serialized_attributes[attr_name]
- end
-
- # Returns the unserialized object of the attribute.
- def unserialize_attribute(attr_name)
- unserialized_object = object_from_yaml(@attributes[attr_name])
-
- if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
- else
- raise SerializationTypeMismatch,
- "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
- end
+ _attributes[attr_name]
end
private
@@ -12,48 +12,20 @@ module TimeZoneConversion
end
module ClassMethods
+
+ def cache_attribute?(attr_name)
+ time_zone_aware?(attr_name) || super
+ end
+
protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced read method automatically converts the UTC time stored in the database to the time 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 = <<-EOV
- def #{attr_name}(reload = false)
- cached = @attributes_cache['#{attr_name}']
- return cached if cached && !reload
- time = read_attribute('#{attr_name}')
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
- end
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
- 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 = <<-EOV
- def #{attr_name}=(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}, time)
- end
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
- else
- super
- end
+ def time_zone_aware?(attr_name)
+ column = columns_hash[attr_name]
+ time_zone_aware_attributes &&
+ !skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) &&
+ [:datetime, :timestamp].include?(column.type)
end
- private
- def create_time_zone_conversion_attribute?(name, column)
- time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
- end
end
end
end
@@ -17,14 +17,9 @@ def define_method_attribute=(attr_name)
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
# columns are turned into +nil+.
def write_attribute(attr_name, value)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id'
+ attr_name = _attributes.unalias(attr_name)
@attributes_cache.delete(attr_name)
- if (column = column_for_attribute(attr_name)) && column.number?
- @attributes[attr_name] = convert_number_column_value(value)
- else
- @attributes[attr_name] = value
- end
+ _attributes[attr_name] = value
end
private
@@ -0,0 +1,37 @@
+module ActiveRecord
+ module Attributes
+
+ # Returns true if the given attribute is in the attributes hash
+ def has_attribute?(attr_name)
+ _attributes.key?(attr_name)
+ end
+
+ # Returns an array of names for the attributes available on this object sorted alphabetically.
+ def attribute_names
+ _attributes.keys.sort!
+ end
+
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ def attributes
+ attributes = _attributes.dup
+ attributes.typecast! unless _attributes.frozen?
+ attributes.to_h
+ end
+
+ protected
+
+ # Not to be confused with the public #attributes method, which returns a typecasted Hash.
+ def _attributes
+ @attributes
+ end
+
+ def initialize_attribute_store(merge_attributes = nil)
+ @attributes = ActiveRecord::Attributes::Store.new
+ @attributes.merge!(merge_attributes) if merge_attributes
+ @attributes.types.merge!(self.class.attribute_types)
+ @attributes.aliases.merge!('id' => self.class.primary_key) unless 'id' == self.class.primary_key
+ @attributes
+ end
+
+ end
+end
@@ -0,0 +1,42 @@
+module ActiveRecord
+ module Attributes
+ module Aliasing
+ # Allows access to keys using aliased names.
+ #
+ # Example:
+ # class Attributes < Hash
+ # include Aliasing
+ # end
+ #
+ # attributes = Attributes.new
+ # attributes.aliases['id'] = 'fancy_primary_key'
+ # attributes['fancy_primary_key'] = 2020
+ #
+ # attributes['id']
+ # => 2020
+ #
+ # Additionally, symbols are always aliases of strings:
+ # attributes[:fancy_primary_key]
+ # => 2020
+ #
+ def [](key)
+ super(unalias(key))
+ end
+
+ def []=(key, value)
+ super(unalias(key), value)
+ end
+
+ def aliases
+ @aliases ||= {}
+ end
+
+ def unalias(key)
+ key = key.to_s
+ aliases[key] || key
+ end
+
+ end
+ end
+end
+
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Attributes
+ class Store < Hash
+ include ActiveRecord::Attributes::Typecasting
+ include ActiveRecord::Attributes::Aliasing
+
+ # Attributes not mapped to a column are handled using Type::Unknown,
+ # which enables boolean typecasting for unmapped keys.
+ def types
+ @types ||= Hash.new(Type::Unknown.new)
+ end
+
+ end
+ end
+end
Oops, something went wrong.

2 comments on commit f936a1f

Contributor

rubys replied Oct 17, 2009

This change causes decimal objects to be treated as strings, leading to errors in existing applications such as "comparison of String with Float failed" in validation routines such as a the following:

def price_must_be_at_least_a_cent
  errors.add(:price, 'should be at least 0.01') if price.nil? || price < 0.00
end

Full scenario and captured output here:

http://intertwingly.net/projects/AWDwR3/checkdepot.html#section-6.4

Contributor

rubys replied Oct 17, 2009

Please sign in to comment.