Changeset 7315
- Timestamp:
- 08/14/07 08:53:02 (9 months ago)
- Files:
-
- trunk/activerecord/lib/active_record/attribute_methods.rb (modified) (2 diffs)
- trunk/activerecord/lib/active_record/base.rb (modified) (11 diffs)
- trunk/activerecord/lib/active_record/transactions.rb (modified) (1 diff)
- trunk/activerecord/test/associations_test.rb (modified) (1 diff)
- trunk/activerecord/test/base_test.rb (modified) (3 diffs)
- trunk/activerecord/test/finder_test.rb (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/lib/active_record/attribute_methods.rb
r4632 r7315 44 44 end 45 45 46 47 # Contains the names of the generated attribute methods. 48 def generated_methods #:nodoc: 49 @generated_methods ||= Set.new 50 end 51 52 def generated_methods? 53 !generated_methods.empty? 54 end 55 56 # generates all the attribute related methods for columns in the database 57 # accessors, mutators and query methods 58 def define_attribute_methods 59 return if generated_methods? 60 columns_hash.each do |name, column| 61 unless instance_methods.include?(name) 62 if self.serialized_attributes[name] 63 define_read_method_for_serialized_attribute(name) 64 else 65 define_read_method(name.to_sym, name, column) 66 end 67 end 68 69 unless instance_methods.include?("#{name}=") 70 define_write_method(name.to_sym) 71 end 72 73 unless instance_methods.include?("#{name}?") 74 define_question_method(name) 75 end 76 end 77 end 78 alias :define_read_methods :define_attribute_methods 79 80 81 46 82 private 47 83 # Suffixes a, ?, c become regexp /(a|\?|c)$/ … … 55 91 @@attribute_method_suffixes ||= [] 56 92 end 57 end 93 94 # Define an attribute reader method. Cope with nil column. 95 def define_read_method(symbol, attr_name, column) 96 cast_code = column.type_cast_code('v') if column 97 access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" 98 99 unless attr_name.to_s == self.primary_key.to_s 100 access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") 101 end 102 103 evaluate_attribute_method attr_name, "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{access_code}; end; end" 104 end 105 106 # Define read method for serialized attribute. 107 def define_read_method_for_serialized_attribute(attr_name) 108 evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end" 109 end 110 111 # Define an attribute ? method. 112 def define_question_method(attr_name) 113 evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?" 114 end 115 116 def define_write_method(attr_name) 117 evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}=" 118 end 119 120 # Evaluate the definition for an attribute related method 121 def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name) 122 123 unless method_name.to_s == primary_key.to_s 124 generated_methods << method_name 125 end 126 127 begin 128 class_eval(method_definition) 129 rescue SyntaxError => err 130 generated_methods.delete(attr_name) 131 if logger 132 logger.warn "Exception occurred during reader method compilation." 133 logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" 134 logger.warn "#{err.message}" 135 end 136 end 137 end 138 end # ClassMethods 139 140 141 # Allows access to the object attributes, which are held in the @attributes hash, as were 142 # they first-class methods. So a Person class with a name attribute can use Person#name and 143 # Person#name= and never directly use the attributes hash -- except for multiple assigns with 144 # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that 145 # the completed attribute is not nil or 0. 146 # 147 # It's also possible to instantiate related objects, so a Client class belonging to the clients 148 # table with a master_id foreign key can instantiate master through Client#master. 149 def method_missing(method_id, *args, &block) 150 method_name = method_id.to_s 151 152 # If we haven't generated any methods yet, generate them, then 153 # see if we've created the method we're looking for. 154 if !self.class.generated_methods? 155 self.class.define_attribute_methods 156 if self.class.generated_methods.include?(method_name) 157 return self.send(method_id, *args, &block) 158 end 159 end 160 161 if self.class.primary_key.to_s == method_name 162 id 163 elsif md = self.class.match_attribute_method?(method_name) 164 attribute_name, method_type = md.pre_match, md.to_s 165 if @attributes.include?(attribute_name) 166 __send__("attribute#{method_type}", attribute_name, *args, &block) 167 else 168 super 169 end 170 elsif @attributes.include?(method_name) 171 read_attribute(method_name) 172 else 173 super 174 end 175 end 176 177 # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, 178 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 179 def read_attribute(attr_name) 180 attr_name = attr_name.to_s 181 if !(value = @attributes[attr_name]).nil? 182 if column = column_for_attribute(attr_name) 183 if unserializable_attribute?(attr_name, column) 184 unserialize_attribute(attr_name) 185 else 186 column.type_cast(value) 187 end 188 else 189 value 190 end 191 else 192 nil 193 end 194 end 195 196 def read_attribute_before_type_cast(attr_name) 197 @attributes[attr_name] 198 end 199 200 # Returns true if the attribute is of a text column and marked for serialization. 201 def unserializable_attribute?(attr_name, column) 202 column.text? && self.class.serialized_attributes[attr_name] 203 end 204 205 # Returns the unserialized object of the attribute. 206 def unserialize_attribute(attr_name) 207 unserialized_object = object_from_yaml(@attributes[attr_name]) 208 209 if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? 210 @attributes[attr_name] = unserialized_object 211 else 212 raise SerializationTypeMismatch, 213 "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" 214 end 215 end 216 217 218 # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float 219 # columns are turned into nil. 220 def write_attribute(attr_name, value) 221 attr_name = attr_name.to_s 222 @attributes_cache.delete(attr_name) 223 if (column = column_for_attribute(attr_name)) && column.number? 224 @attributes[attr_name] = convert_number_column_value(value) 225 else 226 @attributes[attr_name] = value 227 end 228 end 229 230 231 def query_attribute(attr_name) 232 unless value = read_attribute(attr_name) 233 false 234 else 235 column = self.class.columns_hash[attr_name] 236 if column.nil? 237 if Numeric === value || value !~ /[^0-9]/ 238 !value.to_i.zero? 239 else 240 !value.blank? 241 end 242 elsif column.number? 243 !value.zero? 244 else 245 !value.blank? 246 end 247 end 248 end 249 250 # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and 251 # person.respond_to?("name?") which will all return true. 252 alias :respond_to_without_attributes? :respond_to? 253 def respond_to?(method, include_priv = false) 254 method_name = method.to_s 255 if super 256 return true 257 elsif !self.class.generated_methods? 258 self.class.define_attribute_methods 259 if self.class.generated_methods.include?(method_name) 260 return true 261 end 262 end 263 264 if @attributes.nil? 265 return super 266 elsif @attributes.include?(method_name) 267 return true 268 elsif md = self.class.match_attribute_method?(method_name) 269 return true if @attributes.include?(md.pre_match) 270 end 271 super 272 end 273 58 274 59 275 private 276 277 def missing_attribute(attr_name, stack) 278 raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack 279 end 280 60 281 # Handle *? for method_missing. 61 282 def attribute?(attribute_name) trunk/activerecord/lib/active_record/base.rb
r7278 r7315 36 36 class Rollback < StandardError #:nodoc: 37 37 end 38 39 # Raised when you've tried to access a column, which wasn't 40 # loaded by your finder. Typically this is because :select 41 # has been specified 42 class MissingAttributeError < NoMethodError 43 end 38 44 39 45 class AttributeAssignmentError < ActiveRecordError #:nodoc: … … 343 349 @@allow_concurrency = false 344 350 345 # Determines whether to speed up access by generating optimized reader346 # methods to avoid expensive calls to method_missing when accessing347 # attributes by name. You might want to set this to false in development348 # mode, because the methods would be regenerated on each request.349 cattr_accessor :generate_read_methods, :instance_writer => false350 @@generate_read_methods = true351 352 351 # Specifies the format to use when dumping the database schema with Rails' 353 352 # Rakefile. If :sql, the schema is dumped as (potentially database- … … 876 875 end 877 876 878 # Contains the names of the generated reader methods.879 def read_methods #:nodoc:880 @read_methods ||= Set.new881 end882 883 877 # Resets all the cached information about columns, which will cause them to be reloaded on the next request. 884 878 def reset_column_information 885 read_methods.each { |name| undef_method(name) }886 @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @ read_methods = @inheritance_column = nil879 generated_methods.each { |name| undef_method(name) } 880 @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @generated_methods = @inheritance_column = nil 887 881 end 888 882 … … 1101 1095 1102 1096 object.instance_variable_set("@attributes", record) 1097 object.instance_variable_set("@attributes_cache", Hash.new) 1103 1098 object 1104 1099 end … … 1285 1280 def all_attributes_exists?(attribute_names) 1286 1281 attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } 1287 end 1282 end 1288 1283 1289 1284 def attribute_condition(argument) … … 1640 1635 def initialize(attributes = nil) 1641 1636 @attributes = attributes_from_column_definition 1637 @attributes_cache = {} 1642 1638 @new_record = true 1643 1639 ensure_proper_type … … 1653 1649 column = column_for_attribute(attr_name) 1654 1650 1655 if self.class.generate_read_methods 1656 define_read_method(:id, attr_name, column) 1657 # now that the method exists, call it 1658 self.send attr_name.to_sym 1659 else 1660 read_attribute(attr_name) 1661 end 1651 self.class.send(:define_read_method, :id, attr_name, column) 1652 # now that the method exists, call it 1653 self.send attr_name.to_sym 1654 1662 1655 end 1663 1656 … … 1788 1781 clear_association_cache 1789 1782 @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) 1783 @attributes_cache = {} 1790 1784 self 1791 1785 end … … 1903 1897 end 1904 1898 1905 # For checking respond_to? without searching the attributes (which is faster).1906 alias_method :respond_to_without_attributes?, :respond_to?1907 1908 # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and1909 # person.respond_to?("name?") which will all return true.1910 def respond_to?(method, include_priv = false)1911 if @attributes.nil?1912 return super1913 elsif attr_name = self.class.column_methods_hash[method.to_sym]1914 return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key1915 return false if self.class.read_methods.include?(attr_name)1916 elsif @attributes.include?(method_name = method.to_s)1917 return true1918 elsif md = self.class.match_attribute_method?(method.to_s)1919 return true if @attributes.include?(md.pre_match)1920 end1921 # super must be called at the end of the method, because the inherited respond_to?1922 # would return true for generated readers, even if the attribute wasn't present1923 super1924 end1925 1926 1899 # Just freeze the attributes hash, such that associations are still accessible even on destroyed records. 1927 1900 def freeze … … 1999 1972 end 2000 1973 2001 2002 # Allows access to the object attributes, which are held in the @attributes hash, as were2003 # they first-class methods. So a Person class with a name attribute can use Person#name and2004 # Person#name= and never directly use the attributes hash -- except for multiple assigns with2005 # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that2006 # the completed attribute is not nil or 0.2007 #2008 # It's also possible to instantiate related objects, so a Client class belonging to the clients2009 # table with a master_id foreign key can instantiate master through Client#master.2010 def method_missing(method_id, *args, &block)2011 method_name = method_id.to_s2012 if @attributes.include?(method_name) or2013 (md = /\?$/.match(method_name) and2014 @attributes.include?(query_method_name = md.pre_match) and2015 method_name = query_method_name)2016 if self.class.read_methods.empty? && self.class.generate_read_methods2017 define_read_methods2018 # now that the method exists, call it2019 self.send method_id.to_sym2020 else2021 md ? query_attribute(method_name) : read_attribute(method_name)2022 end2023 elsif self.class.primary_key.to_s == method_name2024 id2025 elsif md = self.class.match_attribute_method?(method_name)2026 attribute_name, method_type = md.pre_match, md.to_s2027 if @attributes.include?(attribute_name)2028 __send__("attribute#{method_type}", attribute_name, *args, &block)2029 else2030 super2031 end2032 else2033 super2034 end2035 end2036 2037 # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,2038 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).2039 def read_attribute(attr_name)2040 attr_name = attr_name.to_s2041 if !(value = @attributes[attr_name]).nil?2042 if column = column_for_attribute(attr_name)2043 if unserializable_attribute?(attr_name, column)2044 unserialize_attribute(attr_name)2045 else2046 column.type_cast(value)2047 end2048 else2049 value2050 end2051 else2052 nil2053 end2054 end2055 2056 def read_attribute_before_type_cast(attr_name)2057 @attributes[attr_name]2058 end2059 2060 # Called on first read access to any given column and generates reader2061 # methods for all columns in the columns_hash if2062 # ActiveRecord::Base.generate_read_methods is set to true.2063 def define_read_methods2064 self.class.columns_hash.each do |name, column|2065 unless respond_to_without_attributes?(name)2066 if self.class.serialized_attributes[name]2067 define_read_method_for_serialized_attribute(name)2068 else2069 define_read_method(name.to_sym, name, column)2070 end2071 end2072 2073 unless respond_to_without_attributes?("#{name}?")2074 define_question_method(name)2075 end2076 end2077 end2078 2079 # Define an attribute reader method. Cope with nil column.2080 def define_read_method(symbol, attr_name, column)2081 cast_code = column.type_cast_code('v') if column2082 access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"2083 2084 unless attr_name.to_s == self.class.primary_key.to_s2085 access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")2086 self.class.read_methods << attr_name2087 end2088 2089 evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"2090 end2091 2092 # Define read method for serialized attribute.2093 def define_read_method_for_serialized_attribute(attr_name)2094 unless attr_name.to_s == self.class.primary_key.to_s2095 self.class.read_methods << attr_name2096 end2097 2098 evaluate_read_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"2099 end2100 2101 # Define an attribute ? method.2102 def define_question_method(attr_name)2103 unless attr_name.to_s == self.class.primary_key.to_s2104 self.class.read_methods << "#{attr_name}?"2105 end2106 2107 evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end"2108 end2109 2110 # Evaluate the definition for an attribute reader or ? method2111 def evaluate_read_method(attr_name, method_definition)2112 begin2113 self.class.class_eval(method_definition)2114 rescue SyntaxError => err2115 self.class.read_methods.delete(attr_name)2116 if logger2117 logger.warn "Exception occurred during reader method compilation."2118 logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"2119 logger.warn "#{err.message}"2120 end2121 end2122 end2123 2124 # Returns true if the attribute is of a text column and marked for serialization.2125 def unserializable_attribute?(attr_name, column)2126 column.text? && self.class.serialized_attributes[attr_name]2127 end2128 2129 # Returns the unserialized object of the attribute.2130 def unserialize_attribute(attr_name)2131 unserialized_object = object_from_yaml(@attributes[attr_name])2132 2133 if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?2134 @attributes[attr_name] = unserialized_object2135 else2136 raise SerializationTypeMismatch,2137 "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"2138 end2139 end2140 2141 # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float2142 # columns are turned into nil.2143 def write_attribute(attr_name, value)2144 attr_name = attr_name.to_s2145 if (column = column_for_attribute(attr_name)) && column.number?2146 @attributes[attr_name] = convert_number_column_value(value)2147 else2148 @attributes[attr_name] = value2149 end2150 end2151 2152 1974 def convert_number_column_value(value) 2153 1975 case value … … 2156 1978 when '': nil 2157 1979 else value 2158 end2159 end2160 2161 def query_attribute(attr_name)2162 unless value = read_attribute(attr_name)2163 false2164 else2165 column = self.class.columns_hash[attr_name]2166 if column.nil?2167 if Numeric === value || value !~ /[^0-9]/2168 !value.to_i.zero?2169 else2170 !value.blank?2171 end2172 elsif column.number?2173 !value.zero?2174 else2175 !value.blank?2176 end2177 1980 end 2178 1981 end trunk/activerecord/lib/active_record/transactions.rb
r6439 r7315 122 122 else 123 123 @attributes.delete(self.class.primary_key) 124 end 124 @attributes_cache.delete(self.class.primary_key) 125 end 125 126 raise 126 127 end trunk/activerecord/test/associations_test.rb
r7279 r7315 1471 1471 assert project.respond_to?("name?") 1472 1472 assert project.respond_to?("joined_on") 1473 assert project.respond_to?("joined_on=") 1473 # given that the 'join attribute' won't be persisted, I don't 1474 # think we should define the mutators 1475 #assert project.respond_to?("joined_on=") 1474 1476 assert project.respond_to?("joined_on?") 1475 1477 assert project.respond_to?("access_level") 1476 assert project.respond_to?("access_level=")1478 #assert project.respond_to?("access_level=") 1477 1479 assert project.respond_to?("access_level?") 1478 1480 end trunk/activerecord/test/base_test.rb
r7278 r7315 345 345 end 346 346 347 def test_reader_generation348 Topic.find(:first).title349 Firm.find(:first).name350 Client.find(:first).name351 if ActiveRecord::Base.generate_read_methods352 assert_readers(Topic, %w(type replies_count))353 assert_readers(Firm, %w(type))354 assert_readers(Client, %w(type ruby_type rating?))355 else356 [Topic, Firm, Client].each {|klass| assert_equal klass.read_methods, {}}357 end358 end359 347 360 348 def test_reader_for_invalid_column_names 361 # column names which aren't legal ruby ids 362 topic = Topic.find(:first) 363 topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil) 364 assert !Topic.read_methods.include?("mumub-jumbo") 349 Topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil) 350 assert !Topic.generated_methods.include?("mumub-jumbo") 365 351 end 366 352 … … 792 778 def test_mass_assignment_protection_against_class_attribute_writers 793 779 [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :colorize_logging, 794 :default_timezone, :allow_concurrency, : generate_read_methods, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method|780 :default_timezone, :allow_concurrency, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method| 795 781 assert Task.respond_to?(method) 796 782 assert Task.respond_to?("#{method}=") … … 1709 1695 assert_equal '"This is some really long content, longer than 50 ch..."', t.attribute_for_inspect(:content) 1710 1696 end 1711 1712 private1713 def assert_readers(model, exceptions)1714 expected_readers = Set.new(model.column_names - ['id'])1715 expected_readers += expected_readers.map { |col| "#{col}?" }1716 expected_readers -= exceptions1717 assert_equal expected_readers, model.read_methods1718 end1719 1697 end trunk/activerecord/test/finder_test.rb
r7167 r7315 1 1 require 'abstract_unit' 2 require 'fixtures/author' 2 3 require 'fixtures/comment' 3 4 require 'fixtures/company' … … 130 131 def test_find_only_some_columns 131 132 topic = Topic.find(1, :select => "author_name") 132 assert_raises( NoMethodError) { topic.title}133 assert_raises(ActiveRecord::MissingAttributeError) {topic.title} 133 134 assert_equal "David", topic.author_name 134 135 assert !topic.attribute_present?("title") 135 assert !topic.respond_to?("title")136 #assert !topic.respond_to?("title") 136 137 assert topic.attribute_present?("author_name") 137 138 assert topic.respond_to?("author_name")