Ticket #3238: changed_attributes2.patch
| File changed_attributes2.patch, 12.1 kB (added by argv <lsegal@holycraplions.com>, 3 years ago) |
|---|
-
test/base_test.rb
old new 1104 1104 developers = Developer.find(:all, :order => 'id') 1105 1105 assert_equal 10, developers.size 1106 1106 end 1107 1108 def test_attribute_changed_without_inplace_modifications 1109 # Turn off in place modifications 1110 Topic.track_inplace_modifications = false 1111 1112 # Test for mass attribute and creation 1113 topic = Topic.new(:title => "Test Topic", :content => "Is this thing on?", 1114 :author_name => "Loren", :author_email_address => "lsegal@holycraplions.com") 1115 assert topic.title_changed? 1116 assert topic.author_email_address_changed? 1117 assert_equal 4, topic.changed_attributes.size 1118 topic.save 1119 assert !topic.attribute_changed?(:title) 1120 assert_equal 0, topic.changed_attributes.size 1121 1122 # Make sure inplace modifications don't log changes here 1123 topic.title << " - Some other stuff" 1124 assert_equal "Test Topic - Some other stuff", topic.title 1125 assert !topic.title_changed? 1126 1127 # Test for an update 1128 topic.title = "We changed the topic" 1129 assert topic.title_changed? 1130 assert_equal ["title"], topic.changed_attributes 1131 topic.save 1132 assert !topic.title_changed? 1133 assert_equal 0, topic.changed_attributes.size 1134 1135 # Test for an existing record update 1136 topic = Topic.find(:first) 1137 assert !topic.content_changed? 1138 assert_equal 0, topic.changed_attributes.size 1139 topic.content = "We changed the content this time." 1140 assert topic.content_changed? 1141 assert_equal ["content"], topic.changed_attributes 1142 topic.save 1143 assert !topic.content_changed? 1144 assert_equal 0, topic.changed_attributes.size 1145 1146 # Turn default behaviour back on 1147 Topic.track_inplace_modifications = true 1148 end 1107 1149 1150 def test_reset_changed_attributes 1151 topic = Topic.find(:first) 1152 topic.author_name = "Johnny" 1153 assert topic.author_name_changed? 1154 assert_equal ["author_name"], topic.changed_attributes 1155 1156 # Reload and check if attributes reset 1157 topic.reload 1158 assert !topic.author_name_changed? 1159 end 1160 1161 def test_attribute_changed_with_inplace_modifications 1162 # Test for mass attribute and creation 1163 topic = Topic.new(:title => "Test Topic", :content => "Is this thing on?", 1164 :author_name => "FooBar", :author_email_address => "foo@bar.com") 1165 topic.class.track_inplace_modifications = true 1166 assert topic.class.track_inplace_modifications 1167 1168 assert topic.title_changed? 1169 assert topic.content_changed? 1170 assert topic.author_name_changed? 1171 assert topic.author_email_address_changed? 1172 topic.save 1173 assert !topic.title_changed? 1174 assert_equal 0, topic.changed_attributes.size 1175 1176 # Test for an update 1177 topic.title << " - No, really." 1178 assert topic.title_changed? 1179 assert_equal ["title"], topic.changed_attributes 1180 topic.save 1181 assert !topic.title_changed? 1182 assert_equal 0, topic.changed_attributes.size 1183 1184 # Test for an existing record update 1185 topic = Topic.find(:first) 1186 assert topic.track_inplace_modifications 1187 assert !topic.content_changed? 1188 assert_equal 0, topic.changed_attributes.size 1189 topic.content.gsub!(/./, '') 1190 assert topic.content_changed? 1191 assert_equal ["content"], topic.changed_attributes 1192 topic.save 1193 assert !topic.content_changed? 1194 assert_equal 0, topic.changed_attributes.size 1195 end 1196 1108 1197 # FIXME: this test ought to run, but it needs to run sandboxed so that it 1109 1198 # doesn't b0rk the current test environment by undefing everything. 1110 1199 # -
lib/active_record/base.rb
old new 323 323 cattr_accessor :generate_read_methods 324 324 @@generate_read_methods = true 325 325 326 # Determines whether or not to track in place modifications of attributes 327 # Turning this feature on will raise memory usage of each ActiveRecord object 328 # and take a performance hit for each attribute read, but will allow for 329 # more ruby-like syntax such as: 330 # 331 # t = Topic.find_first 332 # t.title << " Extra information" 333 # t.title.gsub!('Extra', '') 334 # 335 # This feature can be left off and you will still have the ability to use the above 336 # code, but you will not be able to guarantee proper results from attribute_changed? 337 # and can cause problems with record saving. The default value is true. 338 cattr_accessor :track_inplace_modifications 339 @@track_inplace_modifications = true 340 326 341 # Specifies the format to use when dumping the database schema with Rails' 327 342 # Rakefile. If :sql, the schema is dumped as (potentially database- 328 343 # specific) SQL statements. If :ruby, the schema is dumped as an … … 906 921 end 907 922 908 923 object.instance_variable_set("@attributes", record) 924 object.instance_eval("reset_changed_attributes") 909 925 object 910 926 end 911 927 … … 1189 1205 def initialize(attributes = nil) 1190 1206 @attributes = attributes_from_column_definition 1191 1207 @new_record = true 1208 reset_changed_attributes 1192 1209 ensure_proper_type 1193 1210 self.attributes = attributes unless attributes.nil? 1194 1211 yield self if block_given? … … 1228 1245 # * A record does exist: Updates the record with values matching those of the object attributes. 1229 1246 def save 1230 1247 raise ActiveRecord::ReadOnlyRecord if readonly? 1248 freeze_changed_attributes 1231 1249 create_or_update 1250 reset_changed_attributes 1232 1251 end 1233 1252 1234 1253 # Deletes the record in the database and freezes this instance to reflect that no changes should … … 1311 1330 def reload 1312 1331 clear_aggregation_cache 1313 1332 clear_association_cache 1333 reset_changed_attributes 1314 1334 @attributes.update(self.class.find(self.id).instance_variable_get('@attributes')) 1315 1335 self 1316 1336 end … … 1365 1385 def has_attribute?(attr_name) 1366 1386 @attributes.has_key?(attr_name.to_s) 1367 1387 end 1388 1389 # Returns true if the given attribute has been modified 1390 def attribute_changed?(attr_name) 1391 attr_name = attr_name.to_s unless attr_name.is_a?(String) 1392 if @attributes_changed[attr_name].kind_of?(TrueClass) 1393 true 1394 elsif @attributes_changed.has_key?(attr_name) 1395 @attributes_changed[attr_name] != @attributes[attr_name] 1396 else 1397 false 1398 end 1399 end 1400 1401 # Returns an array of attributes modified since last database read or save 1402 def changed_attributes 1403 @attributes_changed.keys.collect {|key| attribute_changed?(key) ? key : nil }.compact 1404 end 1368 1405 1369 1406 # Returns an array of names for the attributes available on this object sorted alphabetically. 1370 1407 def attribute_names … … 1406 1443 return false if self.class.read_methods.include?(attr_name) 1407 1444 elsif @attributes.include?(method_name = method.to_s) 1408 1445 return true 1409 elsif md = /(=|\?|_before_type_cast )$/.match(method_name)1446 elsif md = /(=|\?|_before_type_cast|_changed\?)$/.match(method_name) 1410 1447 return true if @attributes.include?(md.pre_match) 1411 1448 end 1412 1449 # super must be called at the end of the method, because the inherited respond_to? … … 1438 1475 1439 1476 # Updates the associated record with values matching those of the instance attributes. 1440 1477 def update 1478 return true if changed_attributes.empty? 1441 1479 connection.update( 1442 1480 "UPDATE #{self.class.table_name} " + 1443 "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false ))} " +1481 "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, true))} " + 1444 1482 "WHERE #{self.class.primary_key} = #{quote(id)}", 1445 1483 "#{self.class.name} Update" 1446 1484 ) … … 1488 1526 read_attribute(method_name) 1489 1527 elsif self.class.primary_key.to_s == method_name 1490 1528 id 1491 elsif md = /(=|\?|_before_type_cast )$/.match(method_name)1529 elsif md = /(=|\?|_before_type_cast|_changed\?)$/.match(method_name) 1492 1530 attribute_name, method_type = md.pre_match, md.to_s 1493 1531 if @attributes.include?(attribute_name) 1494 1532 case method_type … … 1498 1536 query_attribute(attribute_name) 1499 1537 when '_before_type_cast' 1500 1538 read_attribute_before_type_cast(attribute_name) 1539 when '_changed?' 1540 attribute_changed?(attribute_name) 1501 1541 end 1502 1542 else 1503 1543 super … … 1511 1551 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 1512 1552 def read_attribute(attr_name) 1513 1553 attr_name = attr_name.to_s 1554 track_attribute(attr_name) if track_inplace_modifications 1514 1555 if !(value = @attributes[attr_name]).nil? 1515 1556 if column = column_for_attribute(attr_name) 1516 1557 if unserializable_attribute?(attr_name, column) … … 1527 1568 end 1528 1569 1529 1570 def read_attribute_before_type_cast(attr_name) 1571 track_attribute(attr_name) if track_inplace_modifications 1530 1572 @attributes[attr_name] 1531 1573 end 1532 1574 … … 1552 1594 end 1553 1595 1554 1596 begin 1555 self.class.class_eval("def #{symbol}; #{access_code}; end") 1597 self.class.class_eval("def #{symbol}_changed?; attribute_changed?('#{attr_name}'); end") 1598 self.class.class_eval("def #{symbol}; track_attribute('#{attr_name}') if track_inplace_modifications; #{access_code}; end") 1556 1599 rescue SyntaxError => err 1557 1600 self.class.read_methods.delete(attr_name) 1558 1601 if logger … … 1589 1632 else 1590 1633 @attributes[attr_name] = value 1591 1634 end 1635 @attributes_changed[attr_name] = true unless @attributes_changed.frozen? 1592 1636 end 1593 1637 1594 1638 def convert_number_column_value(value) … … 1637 1681 default << 'id' unless self.class.primary_key.eql? 'id' 1638 1682 default 1639 1683 end 1684 1685 # Resets attributes that have been modified since last database access or save 1686 def reset_changed_attributes 1687 @attributes_changed = {} 1688 end 1689 1690 # Freeze the changed attributes before a save so successive reads won't alter data before 1691 # it goes into the table. 1692 def freeze_changed_attributes 1693 @attributes_changed.freeze 1694 end 1695 1696 # Keeps track of attributes when a value is read for pre-emptive caching of data 1697 # so that we can monitor if the value was changed when track_inplace_modifications is on 1698 def track_attribute(attr_name) 1699 return if @attributes_changed.frozen? || !track_inplace_modifications 1700 attr_name = attr_name.to_s unless attr_name.is_a? String 1701 @attributes_changed[attr_name] = calculate_attribute_hash(attr_name) 1702 end 1703 1704 # Calculates the hash value for the attribute's data. Only used when track_inplace_modifications 1705 # is on. The current scheme uses the cloned attribute as a 'hash' 1706 def calculate_attribute_hash(attr_name) 1707 @attributes[attr_name.to_s].to_s.clone 1708 end 1640 1709 1641 1710 # Returns copy of the attributes hash where all the values have been safely quoted for use in 1642 # an SQL statement. 1643 def attributes_with_quotes(include_primary_key = true) 1711 # an SQL statement. Can return only attributes that have been modified for an update of only 1712 # those specific values. 1713 def attributes_with_quotes(include_primary_key = true, only_changed_attributes = false) 1644 1714 attributes.inject({}) do |quoted, (name, value)| 1645 if column = column_for_attribute(name) 1646 quoted[name] = quote(value, column) unless !include_primary_key && column.primary 1715 unless only_changed_attributes && !attribute_changed?(name) 1716 if column = column_for_attribute(name) 1717 quoted[name] = quote(value, column) unless !include_primary_key && column.primary 1718 end 1647 1719 end 1648 1720 quoted 1649 1721 end