Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #3238: changed_attributes2.patch

File changed_attributes2.patch, 12.1 kB (added by argv <lsegal@holycraplions.com>, 3 years ago)

This patch supercedes the original changed_attributes.patch

  • test/base_test.rb

    old new  
    11041104    developers = Developer.find(:all, :order => 'id') 
    11051105    assert_equal 10, developers.size 
    11061106  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 
    11071149   
     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 
    11081197  # FIXME: this test ought to run, but it needs to run sandboxed so that it 
    11091198  # doesn't b0rk the current test environment by undefing everything. 
    11101199  # 
  • lib/active_record/base.rb

    old new  
    323323    cattr_accessor :generate_read_methods 
    324324    @@generate_read_methods = true 
    325325     
     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     
    326341    # Specifies the format to use when dumping the database schema with Rails' 
    327342    # Rakefile.  If :sql, the schema is dumped as (potentially database- 
    328343    # specific) SQL statements.  If :ruby, the schema is dumped as an  
     
    906921            end 
    907922 
    908923          object.instance_variable_set("@attributes", record) 
     924          object.instance_eval("reset_changed_attributes") 
    909925          object 
    910926        end 
    911927 
     
    11891205      def initialize(attributes = nil) 
    11901206        @attributes = attributes_from_column_definition 
    11911207        @new_record = true 
     1208        reset_changed_attributes 
    11921209        ensure_proper_type 
    11931210        self.attributes = attributes unless attributes.nil? 
    11941211        yield self if block_given? 
     
    12281245      # * A record does exist: Updates the record with values matching those of the object attributes. 
    12291246      def save 
    12301247        raise ActiveRecord::ReadOnlyRecord if readonly? 
     1248        freeze_changed_attributes 
    12311249        create_or_update 
     1250        reset_changed_attributes 
    12321251      end 
    12331252 
    12341253      # Deletes the record in the database and freezes this instance to reflect that no changes should 
     
    13111330      def reload 
    13121331        clear_aggregation_cache 
    13131332        clear_association_cache 
     1333        reset_changed_attributes 
    13141334        @attributes.update(self.class.find(self.id).instance_variable_get('@attributes')) 
    13151335        self 
    13161336      end 
     
    13651385      def has_attribute?(attr_name) 
    13661386        @attributes.has_key?(attr_name.to_s) 
    13671387      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 
    13681405 
    13691406      # Returns an array of names for the attributes available on this object sorted alphabetically. 
    13701407      def attribute_names 
     
    14061443          return false if self.class.read_methods.include?(attr_name) 
    14071444        elsif @attributes.include?(method_name = method.to_s) 
    14081445          return true 
    1409         elsif md = /(=|\?|_before_type_cast)$/.match(method_name) 
     1446        elsif md = /(=|\?|_before_type_cast|_changed\?)$/.match(method_name) 
    14101447          return true if @attributes.include?(md.pre_match) 
    14111448        end 
    14121449        # super must be called at the end of the method, because the inherited respond_to? 
     
    14381475 
    14391476      # Updates the associated record with values matching those of the instance attributes. 
    14401477      def update 
     1478        return true if changed_attributes.empty? 
    14411479        connection.update( 
    14421480          "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))} " + 
    14441482          "WHERE #{self.class.primary_key} = #{quote(id)}", 
    14451483          "#{self.class.name} Update" 
    14461484        ) 
     
    14881526          read_attribute(method_name) 
    14891527        elsif self.class.primary_key.to_s == method_name 
    14901528          id 
    1491         elsif md = /(=|\?|_before_type_cast)$/.match(method_name) 
     1529        elsif md = /(=|\?|_before_type_cast|_changed\?)$/.match(method_name) 
    14921530          attribute_name, method_type = md.pre_match, md.to_s 
    14931531          if @attributes.include?(attribute_name) 
    14941532            case method_type 
     
    14981536                query_attribute(attribute_name) 
    14991537              when '_before_type_cast' 
    15001538                read_attribute_before_type_cast(attribute_name) 
     1539              when '_changed?' 
     1540                attribute_changed?(attribute_name) 
    15011541            end 
    15021542          else 
    15031543            super 
     
    15111551      # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 
    15121552      def read_attribute(attr_name) 
    15131553        attr_name = attr_name.to_s 
     1554        track_attribute(attr_name) if track_inplace_modifications 
    15141555        if !(value = @attributes[attr_name]).nil? 
    15151556          if column = column_for_attribute(attr_name) 
    15161557            if unserializable_attribute?(attr_name, column) 
     
    15271568      end 
    15281569 
    15291570      def read_attribute_before_type_cast(attr_name) 
     1571        track_attribute(attr_name) if track_inplace_modifications 
    15301572        @attributes[attr_name] 
    15311573      end 
    15321574 
     
    15521594        end 
    15531595 
    15541596        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") 
    15561599        rescue SyntaxError => err 
    15571600          self.class.read_methods.delete(attr_name) 
    15581601          if logger 
     
    15891632        else 
    15901633          @attributes[attr_name] = value 
    15911634        end 
     1635        @attributes_changed[attr_name] = true unless @attributes_changed.frozen? 
    15921636      end 
    15931637 
    15941638      def convert_number_column_value(value) 
     
    16371681        default << 'id' unless self.class.primary_key.eql? 'id' 
    16381682        default 
    16391683      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 
    16401709 
    16411710      # 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) 
    16441714        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 
    16471719          end 
    16481720          quoted 
    16491721        end