提交 f218771d 编写于 作者: M Marcel Molina

Add option (true by default) to generate reader methods for each attribute of...

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.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2483 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
上级 c0899bca
*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
\ No newline at end of file
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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册