diff options
7 files changed, 152 insertions, 59 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 7100989b1e..d38ac39282 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -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] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f6219a5a98..72b0164047 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -300,6 +300,13 @@ module ActiveRecord #:nodoc: 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 @@ module ActiveRecord #:nodoc: 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 @@ module ActiveRecord #:nodoc: # 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 @@ module ActiveRecord #:nodoc: 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 @@ module ActiveRecord #:nodoc: @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 @@ module ActiveRecord #:nodoc: # 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) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index c7e7d83ca1..acfd0f0846 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -7,8 +7,8 @@ module ActiveRecord 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 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 1a633dcc00..d318e07750 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -48,22 +48,38 @@ module ActiveRecord # 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 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 8f2b6cda03..8f2129838b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -62,22 +62,24 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb index c1c9ae6f4b..253c8f4e88 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -98,7 +98,7 @@ module ActiveRecord # 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 @@ module ActiveRecord 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 @@ module ActiveRecord 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 diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb index 35c608d809..aeb9ac970a 100755 --- a/activerecord/test/base_test.rb +++ b/activerecord/test/base_test.rb @@ -196,6 +196,19 @@ class BasicsTest < Test::Unit::TestCase 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 @@ class BasicsTest < Test::Unit::TestCase 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 |