From f4853dc17490c88966721ca1ad422baf6ae49745 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 30 Nov 2011 15:52:09 +0000 Subject: Extract attribute serialization code into a separate module --- .../attribute_methods/serialization.rb | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 activerecord/lib/active_record/attribute_methods/serialization.rb (limited to 'activerecord/lib/active_record/attribute_methods/serialization.rb') diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb new file mode 100644 index 0000000000..686754fbc2 --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module AttributeMethods + module Serialization + extend ActiveSupport::Concern + + module ClassMethods + def define_method_attribute(attr_name) + if serialized_attributes.include?(attr_name) + generated_attribute_methods.module_eval(<<-CODE, __FILE__, __LINE__) + def _#{attr_name} + @attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}'] + end + alias #{attr_name} _#{attr_name} + CODE + else + super + end + end + + def cacheable_column?(column) + serialized_attributes.include?(column.name) || super + end + end + + def type_cast_attribute(column, value) + coder = self.class.serialized_attributes[column.name] + + if column.text? && coder + unserialized_object = coder.load(@attributes[column.name]) + + if @attributes.frozen? + unserialized_object + else + @attributes[column.name] = unserialized_object + end + else + super + end + end + end + end +end -- cgit v1.2.3 From 6c63f1aa44780c887dd3e52765e86588911c2802 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 30 Nov 2011 16:00:19 +0000 Subject: Move some serialization stuff out of Base --- .../attribute_methods/serialization.rb | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'activerecord/lib/active_record/attribute_methods/serialization.rb') diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 686754fbc2..001620d889 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -3,7 +3,41 @@ module ActiveRecord module Serialization extend ActiveSupport::Concern + included do + # Returns a hash of all the attributes that have been specified for serialization as + # keys and their class restriction as values. + class_attribute :serialized_attributes + self.serialized_attributes = {} + end + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + def define_method_attribute(attr_name) if serialized_attributes.include?(attr_name) generated_attribute_methods.module_eval(<<-CODE, __FILE__, __LINE__) @@ -22,6 +56,14 @@ module ActiveRecord end end + def set_serialized_attributes + sattrs = self.class.serialized_attributes + + sattrs.each do |key, coder| + @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) + end + end + def type_cast_attribute(column, value) coder = self.class.serialized_attributes[column.name] -- cgit v1.2.3 From 035b4244bae8620515d3757fdc3be42ac77dddec Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 30 Nov 2011 16:12:51 +0000 Subject: Don't need second param --- activerecord/lib/active_record/attribute_methods/serialization.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord/lib/active_record/attribute_methods/serialization.rb') diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 001620d889..ac65ef94f9 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -64,7 +64,7 @@ module ActiveRecord end end - def type_cast_attribute(column, value) + def type_cast_attribute(column) coder = self.class.serialized_attributes[column.name] if column.text? && coder -- cgit v1.2.3 From 7895182d0fce11131024305f53d0cbb32817e65c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 30 Nov 2011 22:31:52 +0000 Subject: omg computer science! Implement a mini state machine for serialized attributes. This means we do not have to deserialize the values upon initialization, which means that if we never actually access the attribute, we never have to deserialize it. --- .../attribute_methods/serialization.rb | 52 ++++++++++++++-------- 1 file changed, 34 insertions(+), 18 deletions(-) (limited to 'activerecord/lib/active_record/attribute_methods/serialization.rb') diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index ac65ef94f9..bc7e9d7a94 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -10,6 +10,26 @@ module ActiveRecord self.serialized_attributes = {} end + class Attribute < Struct.new(:coder, :value, :state) + def unserialized_value + state == :serialized ? unserialize : value + end + + def serialized_value + state == :unserialized ? serialize : value + end + + def unserialize + self.state = :unserialized + self.value = coder.load(value) + end + + def serialize + self.state = :serialized + self.value = coder.dump(value) + end + end + module ClassMethods # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, # then specify the name of that attribute using this method and it will be handled automatically. @@ -42,7 +62,7 @@ module ActiveRecord if serialized_attributes.include?(attr_name) generated_attribute_methods.module_eval(<<-CODE, __FILE__, __LINE__) def _#{attr_name} - @attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}'] + @attributes['#{attr_name}'].unserialized_value end alias #{attr_name} _#{attr_name} CODE @@ -50,31 +70,27 @@ module ActiveRecord super end end - - def cacheable_column?(column) - serialized_attributes.include?(column.name) || super - end end def set_serialized_attributes - sattrs = self.class.serialized_attributes - - sattrs.each do |key, coder| - @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) + self.class.serialized_attributes.each do |key, coder| + if @attributes.key?(key) + @attributes[key] = Attribute.new(coder, @attributes[key], :serialized) + end end end def type_cast_attribute(column) - coder = self.class.serialized_attributes[column.name] - - if column.text? && coder - unserialized_object = coder.load(@attributes[column.name]) + if column.text? && self.class.serialized_attributes.include?(column.name) + @attributes[column.name].unserialized_value + else + super + end + end - if @attributes.frozen? - unserialized_object - else - @attributes[column.name] = unserialized_object - end + def type_cast_attribute_for_write(column, attr_name, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) else super end -- cgit v1.2.3 From d5f7884dc5e74cd876f031ea2d4467cec18defbb Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 30 Nov 2011 22:45:35 +0000 Subject: Don't check column type, you might implement a custom coder that serializes to a different type --- activerecord/lib/active_record/attribute_methods/serialization.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord/lib/active_record/attribute_methods/serialization.rb') diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index bc7e9d7a94..b084fdc25b 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -81,7 +81,7 @@ module ActiveRecord end def type_cast_attribute(column) - if column.text? && self.class.serialized_attributes.include?(column.name) + if self.class.serialized_attributes.include?(column.name) @attributes[column.name].unserialized_value else super -- cgit v1.2.3 From 7a4949e7d5be37f64bf6a1f9dda6f427fbb5ac40 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 30 Nov 2011 23:03:45 +0000 Subject: consistency --- activerecord/lib/active_record/attribute_methods/serialization.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'activerecord/lib/active_record/attribute_methods/serialization.rb') diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index b084fdc25b..d1eb3d5bfc 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -80,15 +80,15 @@ module ActiveRecord end end - def type_cast_attribute(column) - if self.class.serialized_attributes.include?(column.name) - @attributes[column.name].unserialized_value + def type_cast_attribute(column, value) + if column && self.class.serialized_attributes[column.name] + value.unserialized_value else super end end - def type_cast_attribute_for_write(column, attr_name, value) + def type_cast_attribute_for_write(column, value) if column && coder = self.class.serialized_attributes[column.name] Attribute.new(coder, value, :unserialized) else -- cgit v1.2.3