Ticket #3238: activerecord_monitor_changes_fixed.diff
| File activerecord_monitor_changes_fixed.diff, 13.1 kB (added by michaelboutros, 7 months ago) |
|---|
-
activerecord/test/attribute_methods_test.rb
old new 139 139 end 140 140 end 141 141 142 def test_attribute_changed_without_inplace_modifications 143 # Turn off in place modifications 144 Topic.track_inplace_modifications = false 145 146 # Test for mass attribute and creation 147 topic = Topic.new(:title => "Test Topic", :content => "Is this thing on?", 148 :author_name => "Loren", :author_email_address => "lsegal@holycraplions.com") 149 assert topic.title_changed? 150 assert topic.author_email_address_changed? 151 assert_equal 4, topic.changed_attributes.size 152 topic.save 153 assert !topic.attribute_changed?(:title) 154 assert_equal 0, topic.changed_attributes.size 155 156 # Make sure inplace modifications don't log changes here 157 topic.title << " - Some other stuff" 158 assert_equal "Test Topic - Some other stuff", topic.title 159 assert !topic.title_changed? 160 161 # Test for an update 162 topic.title = "We changed the topic" 163 assert topic.title_changed? 164 assert_equal ["title"], topic.changed_attributes 165 topic.save 166 assert !topic.title_changed? 167 assert_equal 0, topic.changed_attributes.size 168 169 # Test for an existing record update 170 topic = Topic.find(:first) 171 assert !topic.content_changed? 172 assert_equal 0, topic.changed_attributes.size 173 topic.content = "We changed the content this time." 174 assert topic.content_changed? 175 assert_equal ["content"], topic.changed_attributes 176 topic.save 177 assert !topic.content_changed? 178 assert_equal 0, topic.changed_attributes.size 179 180 # Turn default behaviour back on 181 Topic.track_inplace_modifications = true 182 end 183 184 def test_reset_changed_attributes 185 topic = Topic.find(:first) 186 topic.author_name = "Johnny" 187 assert topic.author_name_changed? 188 assert_equal ["author_name"], topic.changed_attributes 189 190 # Reload and check if attributes reset 191 topic.reload 192 assert !topic.author_name_changed? 193 end 194 195 def test_attribute_changed_with_inplace_modifications 196 # Test for mass attribute and creation 197 topic = Topic.new(:title => "Test Topic", :content => "Is this thing on?", 198 :author_name => "FooBar", :author_email_address => "foo@bar.com") 199 200 assert topic.class.track_inplace_modifications 201 202 assert topic.title_changed? 203 assert topic.content_changed? 204 assert topic.author_name_changed? 205 assert topic.author_email_address_changed? 206 topic.save 207 assert !topic.title_changed? 208 assert_equal 0, topic.changed_attributes.size 209 210 # Test for an update 211 topic.title << " - No, really." 212 assert topic.title_changed? 213 assert_equal ["title"], topic.changed_attributes 214 topic.save 215 assert !topic.title_changed? 216 assert_equal 0, topic.changed_attributes.size 217 218 # Test for an existing record update 219 topic = Topic.find(:first) 220 assert topic.track_inplace_modifications 221 assert !topic.content_changed? 222 assert_equal 0, topic.changed_attributes.size 223 topic.content.gsub!(/./, '') 224 assert topic.content_changed? 225 assert_equal ["content"], topic.changed_attributes 226 topic.save 227 assert !topic.content_changed? 228 assert_equal 0, topic.changed_attributes.size 229 end 230 142 231 private 143 232 def time_related_columns_on_topic 144 233 Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name) -
activerecord/lib/active_record/attribute_methods.rb
old new 76 76 unless instance_method_already_implemented?("#{name}?") 77 77 define_question_method(name) 78 78 end 79 80 unless instance_method_already_implemented?("#{name}_changed?") 81 define_changed_method(name) 82 end 79 83 end 80 84 end 81 85 … … 149 153 def define_write_method(attr_name) 150 154 evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}=" 151 155 end 152 156 157 def define_changed_method(attr_name) 158 evaluate_attribute_method attr_name, "def #{attr_name}_changed?; attribute_changed?('#{attr_name}'); end", "#{attr_name}_changed?" 159 end 160 153 161 # Evaluate the definition for an attribute related method 154 162 def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name) 155 156 163 unless method_name.to_s == primary_key.to_s 157 164 generated_methods << method_name 158 165 end … … 211 218 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 212 219 def read_attribute(attr_name) 213 220 attr_name = attr_name.to_s 221 track_attribute(attr_name.to_s) 222 214 223 if !(value = @attributes[attr_name]).nil? 215 224 if column = column_for_attribute(attr_name) 216 225 if unserializable_attribute?(attr_name, column) … … 227 236 end 228 237 229 238 def read_attribute_before_type_cast(attr_name) 230 @attributes[attr_name] 239 track_attribute(attr_name) 240 @attributes[attr_name.to_s] 231 241 end 232 242 233 243 # Returns true if the attribute is of a text column and marked for serialization. … … 258 268 else 259 269 @attributes[attr_name] = value 260 270 end 271 272 @attributes_changed[attr_name] = true unless @attributes_changed.frozen? 261 273 end 262 274 263 275 … … 325 337 def attribute_before_type_cast(attribute_name) 326 338 read_attribute_before_type_cast(attribute_name) 327 339 end 340 341 # Handle *_changed? for method_missing 342 def attribute_changed?(attribute_name) 343 attribute_changed?(attribute_name) 344 end 328 345 end 329 346 end -
activerecord/lib/active_record/base.rb
old new 419 419 # Defaults to false. Set to true if you're writing a threaded application. 420 420 cattr_accessor :allow_concurrency, :instance_writer => false 421 421 @@allow_concurrency = false 422 423 # Determines whether or not to track in place modifications of attributes 424 # Turning this feature on will raise memory usage of each ActiveRecord object 425 # and take a performance hit for each attribute read, but will allow for 426 # more ruby-like syntax such as: 427 # 428 # t = Topic.find_first 429 # t.title << " Extra information" 430 # t.title.gsub!('Extra', '') 431 # 432 # This feature can be left off and you will still have the ability to use the above 433 # code, but you will not be able to guarantee proper results from attribute_changed? 434 # and can cause problems with record saving. The default value is true. 435 cattr_accessor :track_inplace_modifications 436 @@track_inplace_modifications = true 422 437 438 423 439 # Specifies the format to use when dumping the database schema with Rails' 424 440 # Rakefile. If :sql, the schema is dumped as (potentially database- 425 441 # specific) SQL statements. If :ruby, the schema is dumped as an … … 1331 1347 end 1332 1348 1333 1349 object.instance_variable_set("@attributes", record) 1350 object.instance_eval("reset_changed_attributes") 1334 1351 object.instance_variable_set("@attributes_cache", Hash.new) 1335 1352 1336 1353 if object.respond_to_without_attributes?(:after_find) … … 1928 1945 @attributes = attributes_from_column_definition 1929 1946 @attributes_cache = {} 1930 1947 @new_record = true 1948 reset_changed_attributes 1931 1949 ensure_proper_type 1932 1950 self.attributes = attributes unless attributes.nil? 1933 1951 self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create) … … 1975 1993 # * No record exists: Creates a new record with values matching those of the object attributes. 1976 1994 # * A record does exist: Updates the record with values matching those of the object attributes. 1977 1995 def save 1996 freeze_changed_attributes 1978 1997 create_or_update 1998 reset_changed_attributes 1979 1999 end 1980 2000 1981 2001 # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a 1982 2002 # RecordNotSaved exception 1983 2003 def save! 2004 freeze_changed_attributes 1984 2005 create_or_update || raise(RecordNotSaved) 2006 reset_changed_attributes 1985 2007 end 1986 2008 1987 2009 # Deletes the record in the database and freezes this instance to reflect that no changes should … … 2088 2110 def reload(options = nil) 2089 2111 clear_aggregation_cache 2090 2112 clear_association_cache 2113 reset_changed_attributes 2091 2114 @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) 2092 2115 @attributes_cache = {} 2093 2116 self … … 2178 2201 @attributes.has_key?(attr_name.to_s) 2179 2202 end 2180 2203 2204 # Returns true if the given attribute has been modified 2205 def attribute_changed?(attr_name) 2206 attr_name = attr_name.to_s unless attr_name.is_a?(String) 2207 if @attributes_changed[attr_name].kind_of?(TrueClass) 2208 true 2209 elsif @attributes_changed.has_key?(attr_name) 2210 @attributes_changed[attr_name] != @attributes[attr_name] 2211 else 2212 false 2213 end 2214 end 2215 2216 # Returns an array of attributes modified since last database read or save. 2217 def changed_attributes 2218 @attributes_changed.keys.collect {|key| attribute_changed?(key) ? key : nil }.compact 2219 end 2220 2181 2221 # Returns an array of names for the attributes available on this object sorted alphabetically. 2182 2222 def attribute_names 2183 2223 @attributes.keys.sort … … 2248 2288 # Updates the associated record with values matching those of the instance attributes. 2249 2289 # Returns the number of affected rows. 2250 2290 def update 2251 quoted_attributes = attributes_with_quotes(false, false) 2291 return true if changed_attributes.empty? 2292 quoted_attributes = attributes_with_quotes(false, false, true) 2252 2293 return 0 if quoted_attributes.empty? 2253 2294 connection.update( 2254 2295 "UPDATE #{self.class.quoted_table_name} " + 2255 "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " + 2296 "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " + 2256 2297 "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}", 2257 2298 "#{self.class.name} Update" 2258 2299 ) … … 2337 2378 default << 'id' unless self.class.primary_key.eql? 'id' 2338 2379 default 2339 2380 end 2381 2382 # Resets attributes that have been modified since last database access or save. 2383 def reset_changed_attributes 2384 @attributes_changed = {} 2385 end 2386 2387 # Freeze the changed attributes before a save so successive reads won't alter data before 2388 # it goes into the table. 2389 def freeze_changed_attributes 2390 @attributes_changed.freeze 2391 end 2392 2393 # Keeps track of attributes when a value is read for pre-emptive caching of data 2394 # so that we can monitor if the value was changed, but only when track_inplace_modifications 2395 # is on. 2396 def track_attribute(attr_name) 2397 return if @attributes_changed.frozen? || !track_inplace_modifications 2398 attr_name = attr_name.to_s unless attr_name.is_a? String 2399 @attributes_changed[attr_name] = calculate_attribute_hash(attr_name) 2400 end 2401 2402 # Calculates the hash value for the attribute's data. Only used when track_inplace_modifications 2403 # is on. The current scheme uses the cloned attribute as a 'hash' 2404 def calculate_attribute_hash(attr_name) 2405 @attributes[attr_name.to_s].to_s.clone 2406 end 2340 2407 2341 2408 # Returns a copy of the attributes hash where all the values have been safely quoted for use in 2342 # an SQL statement. 2343 def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true) 2409 # an SQL statement. Can return only attributes that have been modified for an update of only 2410 # those specific values. 2411 def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, only_changed_attributes = false) 2344 2412 quoted = attributes.inject({}) do |result, (name, value)| 2345 if column = column_for_attribute(name) 2346 result[name] = quote_value(value, column) unless !include_primary_key && column.primary 2413 unless only_changed_attributes && !attribute_changed?(name) 2414 if column = column_for_attribute(name) 2415 result[name] = quote_value(value, column) unless !include_primary_key && column.primary 2416 end 2347 2417 end 2348 2418 result 2349 2419 end