aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/attribute_methods
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/attribute_methods')
-rw-r--r--activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb32
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb88
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb147
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb89
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb23
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb30
7 files changed, 288 insertions, 127 deletions
diff --git a/activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb b/activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb
new file mode 100644
index 0000000000..0eb0db65b1
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb
@@ -0,0 +1,32 @@
+require 'active_support/concern'
+require 'active_support/deprecation'
+
+module ActiveRecord
+ module AttributeMethods
+ module DeprecatedUnderscoreRead
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_prefix "_"
+ end
+
+ module ClassMethods
+ protected
+
+ def define_method__attribute(attr_name)
+ # Do nothing, let it hit method missing instead.
+ end
+ end
+
+ protected
+
+ def _attribute(attr_name)
+ ActiveSupport::Deprecation.warn(
+ "You have called '_#{attr_name}'. This is deprecated. Please use " \
+ "either '#{attr_name}' or read_attribute('#{attr_name}')."
+ )
+ read_attribute(attr_name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 3eff3d54e3..f61e016f46 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -34,9 +34,9 @@ module ActiveRecord
@previously_changed = changes
@changed_attributes.clear
end
- rescue
- IdentityMap.remove(self) if IdentityMap.enabled?
- raise
+ rescue
+ IdentityMap.remove(self) if IdentityMap.enabled?
+ raise
end
# <tt>reload</tt> the record and clears changed attributes.
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index a404a5edd7..5d37088d98 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -5,15 +5,49 @@ module ActiveRecord
# Returns this record's primary key value wrapped in an Array if one is available
def to_key
- key = send(self.class.primary_key)
+ key = self.id
[key] if key
end
+ # Returns the primary key value
+ def id
+ read_attribute(self.class.primary_key)
+ end
+
+ # Sets the primary key value
+ def id=(value)
+ write_attribute(self.class.primary_key, value)
+ end
+
+ # Queries the primary key value
+ def id?
+ query_attribute(self.class.primary_key)
+ end
+
module ClassMethods
+ def define_method_attribute(attr_name)
+ super
+
+ if attr_name == primary_key && attr_name != 'id'
+ generated_attribute_methods.send(:alias_method, :id, primary_key)
+ generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__
+ def id(v, attributes, attributes_cache, attr_name)
+ attr_name = '#{primary_key}'
+ send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name)
+ end
+ CODE
+ end
+ end
+
+ def dangerous_attribute_method?(method_name)
+ super && !['id', 'id=', 'id?'].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.
def primary_key
- @primary_key ||= reset_primary_key
+ @primary_key = reset_primary_key unless defined? @primary_key
+ @primary_key
end
# Returns a quoted version of the primary key name, used to construct SQL statements.
@@ -22,11 +56,11 @@ module ActiveRecord
end
def reset_primary_key #:nodoc:
- key = self == base_class ? get_primary_key(base_class.name) :
- base_class.primary_key
-
- set_primary_key(key)
- key
+ if self == base_class
+ self.primary_key = get_primary_key(base_class.name)
+ else
+ self.primary_key = base_class.primary_key
+ end
end
def get_primary_key(base_name) #:nodoc:
@@ -38,35 +72,41 @@ module ActiveRecord
when :table_name_with_underscore
base_name.foreign_key
else
- if ActiveRecord::Base != self && connection.table_exists?(table_name)
- connection.primary_key(table_name)
+ if ActiveRecord::Base != self && table_exists?
+ connection.schema_cache.primary_keys[table_name]
else
'id'
end
end
end
- attr_accessor :original_primary_key
-
- # Attribute writer for the primary key column
- def primary_key=(value)
- @quoted_primary_key = nil
- @primary_key = value
+ def original_primary_key #:nodoc:
+ deprecated_original_property_getter :primary_key
end
- # Sets the name of the primary key column to use to the given value,
- # or (if the value is nil or false) to the value returned by the given
- # block.
+ # Sets the name of the primary key column.
+ #
+ # class Project < ActiveRecord::Base
+ # self.primary_key = "sysid"
+ # end
+ #
+ # You can also define the primary_key method yourself:
#
# class Project < ActiveRecord::Base
- # set_primary_key "sysid"
+ # def self.primary_key
+ # "foo_" + super
+ # end
# end
- def set_primary_key(value = nil, &block)
+ # Project.primary_key # => "foo_id"
+ def primary_key=(value)
+ @original_primary_key = @primary_key if defined?(@primary_key)
+ @primary_key = value && value.to_s
+ @quoted_primary_key = nil
+ end
+
+ def set_primary_key(value = nil, &block) #:nodoc:
+ deprecated_property_setter :primary_key, value, block
@quoted_primary_key = nil
- @primary_key ||= ''
- self.original_primary_key = @primary_key
- value &&= value.to_s
- self.primary_key = block_given? ? instance_eval(&block) : value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 4a5afcd585..77bf9d0905 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -8,9 +8,6 @@ module ActiveRecord
included do
cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
-
- # Undefine id so it can be used as an attribute name
- undef_method(:id) if method_defined?(:id)
end
module ClassMethods
@@ -32,109 +29,103 @@ module ActiveRecord
cached_attributes.include?(attr_name)
end
+ def undefine_attribute_methods
+ if base_class == self
+ generated_external_attribute_methods.module_eval do
+ instance_methods.each { |m| undef_method(m) }
+ end
+ end
+
+ super
+ 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)
- if serialized_attributes.include?(attr_name)
- define_read_method_for_serialized_attribute(attr_name)
- else
- define_read_method(attr_name, attr_name, columns_hash[attr_name])
- end
+ cast_code = attribute_cast_code(attr_name)
- if attr_name == primary_key && attr_name != "id"
- define_read_method('id', attr_name, columns_hash[attr_name])
- end
+ 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)
- serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type)
- end
-
- # Define read method for serialized attribute.
- def define_read_method_for_serialized_attribute(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']"
- generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__)
+ attribute_types_cached_by_default.include?(column.type)
end
- # Define an attribute reader method. Cope with nil column.
- # method_name is the same as attr_name except when a non-standard primary key is used,
- # we still define #id as an accessor for the key
- def define_read_method(method_name, attr_name, column)
- cast_code = column.type_cast_code('v')
- access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
+ def internal_attribute_access_code(attr_name, cast_code)
+ access_code = "(v=@attributes[attr_name]) && #{cast_code}"
- unless attr_name.to_s == self.primary_key.to_s
- access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+ unless attr_name == primary_key
+ access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ")
end
if cache_attribute?(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
+ access_code = "@attributes_cache[attr_name] ||= (#{access_code})"
end
- # Where possible, generate the method by evalling a string, as this will result in
- # faster accesses because it avoids the block eval and then string eval incurred
- # by the second branch.
- #
- # The second, slower, branch is necessary to support instances where the database
- # returns columns with extra stuff in (like 'my_column(omg)').
- if method_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
- def _#{method_name}
- #{access_code}
- end
-
- alias #{method_name} _#{method_name}
- STR
- else
- generated_attribute_methods.module_eval do
- define_method("_#{method_name}") { eval(access_code) }
- alias_method(method_name, "_#{method_name}")
- end
+ "attr_name = '#{attr_name}'; #{access_code}"
+ end
+
+ def external_attribute_access_code(attr_name, cast_code)
+ access_code = "v && #{cast_code}"
+
+ if cache_attribute?(attr_name)
+ access_code = "attributes_cache[attr_name] ||= (#{access_code})"
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)
- method = "_#{attr_name}"
- if respond_to? method
- send method if @attributes.has_key?(attr_name.to_s)
- else
- _read_attribute attr_name
- end
- end
+ return unless attr_name
- def _read_attribute(attr_name)
attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id'
- value = @attributes[attr_name]
- unless value.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
+ methods = self.class.generated_external_attribute_methods
+
+ if methods.method_defined?(attr_name)
+ if @attributes.has_key?(attr_name) || attr_name == 'id'
+ methods.send(attr_name, @attributes[attr_name], @attributes, @attributes_cache, attr_name)
end
+ elsif !self.class.attribute_methods_generated?
+ # If we haven't generated the caster methods yet, do that and
+ # then try again
+ self.class.define_attribute_methods
+ read_attribute(attr_name)
+ else
+ # If we get here, the attribute has no associated DB column, so
+ # just return it verbatim.
+ @attributes[attr_name]
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.include?(attr_name)
- end
-
- # Returns the unserialized object of the attribute.
- def unserialize_attribute(attr_name)
- coder = self.class.serialized_attributes[attr_name]
- unserialized_object = coder.load(@attributes[attr_name])
-
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
- end
-
private
def attribute(attribute_name)
read_attribute(attribute_name)
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..0a4432506f
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -0,0 +1,89 @@
+module ActiveRecord
+ module AttributeMethods
+ 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
+
+ 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.
+ # 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
+
+ private
+
+ def attribute_cast_code(attr_name)
+ if serialized_attributes.include?(attr_name)
+ "v.unserialized_value"
+ else
+ super
+ end
+ end
+ end
+
+ def set_serialized_attributes
+ 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_for_write(column, value)
+ if column && coder = self.class.serialized_attributes[column.name]
+ Attribute.new(coder, value, :unserialized)
+ 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 62a3cfa9a5..17cf34cdf6 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -16,21 +16,16 @@ module ActiveRecord
module ClassMethods
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
+ # The 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, line = <<-EOV, __LINE__ + 1
- def _#{attr_name}
- cached = @attributes_cache['#{attr_name}']
- return cached if cached
- time = _read_attribute('#{attr_name}')
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
- end
- alias #{attr_name} _#{attr_name}
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
+ 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
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index eb585ee906..fde55b95da 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -17,10 +17,6 @@ module ActiveRecord
write_attribute(attr_name, new_value)
end
end
-
- if attr_name == primary_key && attr_name != "id"
- generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='")
- end
end
end
@@ -32,10 +28,8 @@ module ActiveRecord
@attributes_cache.delete(attr_name)
column = column_for_attribute(attr_name)
- if column && column.number?
- @attributes[attr_name] = convert_number_column_value(value)
- elsif column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = value
+ if column || @attributes.has_key?(attr_name)
+ @attributes[attr_name] = type_cast_attribute_for_write(column, value)
else
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
end
@@ -47,6 +41,26 @@ module ActiveRecord
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 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
end
end
end