Permalink
Browse files

Add option (true by default) to generate reader methods for each attr…

…ibute of a record to avoid the overhead of calling method missing. In partial fullfilment of #1236.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2483 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent c0899bc commit f218771d3e9240337cd309d8396b5479d9ff555d Marcel Molina committed Oct 7, 2005
View
@@ -1,5 +1,7 @@
*SVN*
+* Add option (true by default) to generate reader methods for each attribute of a record to avoid the overhead of calling method missing. In partial fullfilment of #1236. [skaes@web.de]
+
* Add convenience predicate methods on Column class. In partial fullfilment of #1236. [skaes@web.de]
* Raise errors when invalid hash keys are passed to ActiveRecord::Base.find. #2363 [Chad Fowler <chad@chadfowler.com>, Nicholas Seckar]
@@ -300,6 +300,13 @@ def self.reset_subclasses
cattr_accessor :threaded_connections
@@threaded_connections = true
+ # Determines whether to speed up access by generating optimized reader
+ # methods to avoid expensive calls to method_missing when accessing
+ # attributes by name. You might want to set this to false in development
+ # mode, because the methods would be regenerated on each request.
+ cattr_accessor :generate_read_methods
+ @@generate_read_methods = true
+
class << self # Class methods
# Find operates with three different retreval approaches:
#
@@ -683,9 +690,16 @@ def column_methods_hash
end
end
+
+ # Contains the names of the generated reader methods.
+ def read_methods
+ @read_methods ||= {}
+ end
+
# Resets all the cached information about columns, which will cause they to be reloaded on the next request.
def reset_column_information
- @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = nil
+ read_methods.each_key {|name| undef_method(name)}
+ @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @read_methods = nil
end
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
@@ -1016,7 +1030,10 @@ def initialize(attributes = nil)
# Every Active Record class must use "id" as their primary ID. This getter overwrites the native
# id method, which isn't being used in this context.
def id
- read_attribute(self.class.primary_key)
+ attr_name = self.class.primary_key
+ column = column_for_attribute(attr_name)
+ define_read_method(:id, attr_name, column) if self.class.generate_read_methods
+ (value = @attributes[attr_name]) && column.type_cast(value)
end
# Enables Active Record objects to be used as URL parameters in Action Pack automatically.
@@ -1267,6 +1284,7 @@ def ensure_proper_type
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if @attributes.include?(method_name)
+ define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
read_attribute(method_name)
elsif md = /(=|\?|_before_type_cast)$/.match(method_name)
attribute_name, method_type = md.pre_match, md.to_s
@@ -1310,11 +1328,37 @@ def read_attribute_before_type_cast(attr_name)
@attributes[attr_name]
end
+ # Called on first read access to any given column and generates reader
+ # methods for all columns in the columns_hash if
+ # ActiveRecord::Base.generate_read_methods is set to true.
+ def define_read_methods
+ self.class.columns_hash.each do |name, column|
+ unless column.primary || self.class.serialized_attributes[name] || respond_to_without_attributes?(name)
+ define_read_method(name.to_sym, name, column)
+ end
+ end
+ end
+
+ # Define a column type specific reader method.
+ def define_read_method(symbol, attr_name, column)
+ cast_code = column.type_cast_code('v')
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
+ body = access_code
+
+ # The following 3 lines behave exactly like method_missing if the
+ # attribute isn't present.
+ unless symbol == :id
+ body = body.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")
+ end
+ self.class.class_eval("def #{symbol}; #{body} end")
+
+ self.class.read_methods[attr_name] = true unless symbol == :id
+ logger.debug "Defined read method #{self.class.name}.#{symbol}" if logger
+ end
+
# Returns true if the attribute is of a text column and marked for serialization.
def unserializable_attribute?(attr_name, column)
- if value = @attributes[attr_name]
- [:text, :string].include?(column.send(:type)) && value.is_a?(String) && self.class.serialized_attributes[attr_name]
- end
+ column.text? && self.class.serialized_attributes[attr_name]
end
# Returns the unserialized object of the attribute.
@@ -1332,12 +1376,21 @@ def unserialize_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)
- @attributes[attr_name.to_s] = empty_string_for_number_column?(attr_name.to_s, value) ? nil : value
+ attr_name = attr_name.to_s
+ if (column = column_for_attribute(attr_name)) && column.number?
+ @attributes[attr_name] = convert_number_column_value(value)
+ else
+ @attributes[attr_name] = value
+ end
end
- def empty_string_for_number_column?(attr_name, value)
- column = column_for_attribute(attr_name)
- column && (column.klass == Fixnum || column.klass == Float) && value == ""
+ def convert_number_column_value(value)
+ case value
+ when FalseClass: 0
+ when TrueClass: 1
+ when '': nil
+ else value
+ end
end
def query_attribute(attr_name)
@@ -7,8 +7,8 @@ def quote(value, column = nil)
case value
when String
if column && column.type == :binary
- "'#{quote_string(column.string_to_binary(value))}'" # ' (for ruby-mode)
- elsif column && [:integer, :float].include?(column.type)
+ "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
+ elsif column && [:integer, :float].include?(column.type)
value.to_s
else
"'#{quote_string(value)}'" # ' (for ruby-mode)
@@ -48,4 +48,4 @@ def quoted_date(value)
end
end
end
-end
+end
@@ -48,22 +48,38 @@ def klass
# Casts value (which is a String) to an appropriate instance.
def type_cast(value)
- if value.nil? then return nil end
+ return nil if value.nil?
case type
when :string then value
when :text then value
when :integer then value.to_i rescue value ? 1 : 0
when :float then value.to_f
- when :datetime then string_to_time(value)
- when :timestamp then string_to_time(value)
- when :time then string_to_dummy_time(value)
- when :date then string_to_date(value)
- when :binary then binary_to_string(value)
+ when :datetime then self.class.string_to_time(value)
+ when :timestamp then self.class.string_to_time(value)
+ when :time then self.class.string_to_dummy_time(value)
+ when :date then self.class.string_to_date(value)
+ when :binary then self.class.binary_to_string(value)
when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
else value
end
end
+ def type_cast_code(var_name)
+ case type
+ when :string then nil
+ when :text then nil
+ when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
+ when :float then "#{var_name}.to_f"
+ when :datetime then "#{self.class.name}.string_to_time(#{var_name})"
+ when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
+ when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})"
+ when :date then "#{self.class.name}.string_to_date(#{var_name})"
+ when :binary then "#{self.class.name}.binary_to_string(#{var_name})"
+ when :boolean then "(#{var_name} == true or (#{var_name} =~ /^t(?:true)?$/i) == 0 or #{var_name}.to_s == '1')"
+ else nil
+ end
+ end
+
# Returns the human name of the column name.
#
# ===== Examples
@@ -73,38 +89,38 @@ def human_name
end
# Used to convert from Strings to BLOBs
- def string_to_binary(value)
+ def self.string_to_binary(value)
value
end
# Used to convert from BLOBs to Strings
- def binary_to_string(value)
+ def self.binary_to_string(value)
value
end
- private
- def string_to_date(string)
- return string unless string.is_a?(String)
- date_array = ParseDate.parsedate(string)
- # treat 0000-00-00 as nil
- Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
- end
+ def self.string_to_date(string)
+ return string unless string.is_a?(String)
+ date_array = ParseDate.parsedate(string)
+ # treat 0000-00-00 as nil
+ Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
+ end
- def string_to_time(string)
- return string unless string.is_a?(String)
- time_array = ParseDate.parsedate(string)[0..5]
- # treat 0000-00-00 00:00:00 as nil
- Time.send(Base.default_timezone, *time_array) rescue nil
- end
+ def self.string_to_time(string)
+ return string unless string.is_a?(String)
+ time_array = ParseDate.parsedate(string)[0..5]
+ # treat 0000-00-00 00:00:00 as nil
+ Time.send(Base.default_timezone, *time_array) rescue nil
+ end
- def string_to_dummy_time(string)
- return string unless string.is_a?(String)
- time_array = ParseDate.parsedate(string)
- # pad the resulting array with dummy date information
- time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1;
- Time.send(Base.default_timezone, *time_array) rescue nil
- end
+ def self.string_to_dummy_time(string)
+ return string unless string.is_a?(String)
+ time_array = ParseDate.parsedate(string)
+ # pad the resulting array with dummy date information
+ time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1;
+ Time.send(Base.default_timezone, *time_array) rescue nil
+ end
+ private
def extract_limit(sql_type)
$1.to_i if sql_type =~ /\((.*)\)/
end
@@ -62,22 +62,24 @@ def parse_config!(config)
module ConnectionAdapters #:nodoc:
class SQLiteColumn < Column #:nodoc:
- def string_to_binary(value)
- value.gsub(/\0|\%/) do |b|
- case b
- when "\0" then "%00"
- when "%" then "%25"
- end
- end
- end
-
- def binary_to_string(value)
- value.gsub(/%00|%25/) do |b|
- case b
- when "%00" then "\0"
- when "%25" then "%"
- end
- end
+ class << self
+ def string_to_binary(value)
+ value.gsub(/\0|\%/) do |b|
+ case b
+ when "\0" then "%00"
+ when "%" then "%25"
+ end
+ end
+ end
+
+ def binary_to_string(value)
+ value.gsub(/%00|%25/) do |b|
+ case b
+ when "%00" then "\0"
+ when "%25" then "%"
+ end
+ end
+ end
end
end
@@ -98,7 +98,7 @@ def cast_to_datetime(value)
# These methods will only allow the adapter to insert binary data with a length of 7K or less
# because of a SQL Server statement length policy.
- def string_to_binary(value)
+ def self.string_to_binary(value)
value.gsub(/(\r|\n|\0|\x1a)/) do
case $1
when "\r" then "%00"
@@ -109,7 +109,7 @@ def string_to_binary(value)
end
end
- def binary_to_string(value)
+ def self.binary_to_string(value)
value.gsub(/(%00|%01|%02|%03)/) do
case $1
when "%00" then "\r"
@@ -275,7 +275,7 @@ def quote(value, column = nil)
case value
when String
if column && column.type == :binary
- "'#{quote_string(column.string_to_binary(value))}'"
+ "'#{quote_string(column.class.string_to_binary(value))}'"
else
"'#{quote_string(value)}'"
end
@@ -196,6 +196,19 @@ def test_read_attribute_when_false
assert !topic.approved?, "approved should be false"
end
+ def test_reader_generation
+ Topic.find(:first).title
+ Firm.find(:first).name
+ Client.find(:first).name
+ if ActiveRecord::Base.generate_read_methods
+ assert_readers(Topic, %w(type replies_count))
+ assert_readers(Firm, %w(type))
+ assert_readers(Client, %w(type))
+ else
+ [Topic, Firm, Client].each {|klass| assert_equal klass.read_methods, {}}
+ end
+ end
+
def test_preserving_date_objects
# SQL Server doesn't have a separate column type just for dates, so all are returned as time
if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
@@ -913,4 +926,11 @@ def test_clear_association_cache_new_record
assert_equal firm.clients.collect{ |x| x.name }.sort, clients.collect{ |x| x.name }.sort
end
+
+ private
+
+ def assert_readers(model, exceptions)
+ expected_readers = model.column_names - (model.serialized_attributes.keys + exceptions + ['id'])
+ assert_equal expected_readers.sort, model.read_methods.keys.sort
+ end
end

0 comments on commit f218771

Please sign in to comment.