diff options
-rw-r--r-- | activerecord/CHANGELOG | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/aggregations.rb | 4 | ||||
-rwxr-xr-x | activerecord/lib/active_record/base.rb | 8 | ||||
-rwxr-xr-x | activerecord/lib/active_record/callbacks.rb | 4 | ||||
-rw-r--r-- | activerecord/lib/active_record/dirty.rb | 52 | ||||
-rw-r--r-- | activerecord/lib/active_record/locking/optimistic.rb | 9 | ||||
-rw-r--r-- | activerecord/lib/active_record/timestamp.rb | 4 | ||||
-rwxr-xr-x | activerecord/test/cases/associations_test.rb | 3 | ||||
-rwxr-xr-x | activerecord/test/cases/base_test.rb | 1 | ||||
-rw-r--r-- | activerecord/test/cases/dirty_test.rb | 47 | ||||
-rw-r--r-- | activerecord/test/cases/query_cache_test.rb | 4 | ||||
-rw-r--r-- | railties/configs/initializers/new_in_rails_3.rb | 5 | ||||
-rw-r--r-- | railties/lib/rails_generator/generators/applications/app/app_generator.rb | 3 |
13 files changed, 112 insertions, 34 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index ce8edcae9e..cfd3b9fb38 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Partial updates include only unsaved attributes. Off by default; set YourClass.partial_updates = true to enable. [Jeremy Kemper] + * Removing unnecessary uses_tzinfo helper from tests, given that TZInfo is now bundled [Geoff Buesing] * Fixed that validates_size_of :within works in associations #11295, #10019 [cavalle] diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index cad8304bcf..61446cde36 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -174,11 +174,11 @@ module ActiveRecord module_eval do define_method("#{name}=") do |part| if part.nil? && allow_nil - mapping.each { |pair| @attributes[pair.first] = nil } + mapping.each { |pair| self[pair.first] = nil } instance_variable_set("@#{name}", nil) else part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil? - mapping.each { |pair| @attributes[pair.first] = part.send(pair.last) } + mapping.each { |pair| self[pair.first] = part.send(pair.last) } instance_variable_set("@#{name}", part.freeze) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index af480a0797..fe38454226 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -2407,8 +2407,8 @@ module ActiveRecord #:nodoc: # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. - def update - quoted_attributes = attributes_with_quotes(false, false) + def update(attribute_names = @attributes.keys) + quoted_attributes = attributes_with_quotes(false, false, attribute_names) return 0 if quoted_attributes.empty? connection.update( "UPDATE #{self.class.quoted_table_name} " + @@ -2500,10 +2500,10 @@ module ActiveRecord #:nodoc: # Returns a copy of the attributes hash where all the values have been safely quoted for use in # an SQL statement. - def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true) + def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) quoted = {} connection = self.class.connection - @attributes.each_pair do |name, value| + attribute_names.each do |name| if column = column_for_attribute(name) quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 67a4117d20..a469af682b 100755 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -229,9 +229,9 @@ module ActiveRecord # Is called _after_ <tt>Base.save</tt> on existing objects that have a record. def after_update() end - def update_with_callbacks #:nodoc: + def update_with_callbacks(*args) #:nodoc: return false if callback(:before_update) == false - result = update_without_callbacks + result = update_without_callbacks(*args) callback(:after_update) result end diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index 4b65545851..6530500e56 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -28,12 +28,21 @@ module ActiveRecord # person.name = 'bob' # person.changed # => ['name'] # person.changes # => { 'name' => ['Bill', 'bob'] } + # + # Before modifying an attribute in-place: + # person.name_will_change! + # person.name << 'by' + # person.name_change # => ['uncle bob', 'uncle bobby'] module Dirty def self.included(base) - base.attribute_method_suffix '_changed?', '_change', '_was' + base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' base.alias_method_chain :write_attribute, :dirty base.alias_method_chain :save, :dirty base.alias_method_chain :save!, :dirty + base.alias_method_chain :update, :dirty + + base.superclass_delegating_accessor :partial_updates + base.partial_updates = true end # Do any attributes have unsaved changes? @@ -81,6 +90,25 @@ module ActiveRecord @changed_attributes ||= {} end + # Handle *_changed? for method_missing. + def attribute_changed?(attr) + changed_attributes.include?(attr) + end + + # Handle *_change for method_missing. + def attribute_change(attr) + [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) + end + + # Handle *_was for method_missing. + def attribute_was(attr) + attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + end + + # Handle *_will_change! for method_missing. + def attribute_will_change!(attr) + changed_attributes[attr] = clone_attribute_value(:read_attribute, attr) + end # Wrap write_attribute to remember original attribute value. def write_attribute_with_dirty(attr, value) @@ -88,7 +116,7 @@ module ActiveRecord # The attribute already has an unsaved change. unless changed_attributes.include?(attr) - old = read_attribute(attr) + old = clone_attribute_value(:read_attribute, attr) # Remember the original value if it's different. typecasted = if column = column_for_attribute(attr) @@ -103,20 +131,12 @@ module ActiveRecord write_attribute_without_dirty(attr, value) end - - # Handle *_changed? for method_missing. - def attribute_changed?(attr) - changed_attributes.include?(attr) - end - - # Handle *_change for method_missing. - def attribute_change(attr) - [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) - end - - # Handle *_was for method_missing. - def attribute_was(attr) - attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + def update_with_dirty + if partial_updates? + update_without_dirty(changed) + else + update_without_dirty + end end end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 799309c17b..f2c2c5f070 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,17 +66,20 @@ module ActiveRecord return result end - def update_with_lock #:nodoc: - return update_without_lock unless locking_enabled? + def update_with_lock(attribute_names = @attributes.keys) #:nodoc: + return update_without_lock(attribute_names) unless locking_enabled? lock_col = self.class.locking_column previous_value = send(lock_col).to_i send(lock_col + '=', previous_value + 1) + attribute_names += [lock_col] + attribute_names.uniq! + begin affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking") UPDATE #{self.class.table_name} - SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false))} + SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))} WHERE #{self.class.primary_key} = #{quote_value(id)} AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)} end_sql diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 95e29847cb..dc95d2aabb 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -29,13 +29,13 @@ module ActiveRecord create_without_timestamps end - def update_with_timestamps #:nodoc: + def update_with_timestamps(*args) #:nodoc: if record_timestamps t = self.class.default_timezone == :utc ? Time.now.utc : Time.now write_attribute('updated_at', t) if respond_to?(:updated_at) write_attribute('updated_on', t) if respond_to?(:updated_on) end - update_without_timestamps + update_without_timestamps(*args) end end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 3c14ee0881..5a9e32f774 100755 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -449,11 +449,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_not_resaved_when_unchanged firm = Firm.find(:first, :include => :account) + firm.name += '-changed' assert_queries(1) { firm.save! } firm = Firm.find(:first) firm.account = Account.find(:first) - assert_queries(1) { firm.save! } + assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } firm = Firm.find(:first).clone firm.account = Account.find(:first) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 977c5d7333..e5bba1b63d 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -153,6 +153,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 2, Topic.find(topic.id).content["two"] + topic.content_will_change! topic.content["three"] = 3 topic.save diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 61f6ef04aa..356293140c 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' # For booleans require 'models/pirate' # For timestamps +require 'models/parrot' class Pirate # Just reopening it, not defining it attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected @@ -19,7 +20,7 @@ private end end -class DirtyTest < Test::Unit::TestCase +class DirtyTest < ActiveRecord::TestCase def test_attribute_changes # New record - no changes. pirate = Pirate.new @@ -43,7 +44,6 @@ class DirtyTest < Test::Unit::TestCase assert_nil pirate.catchphrase_change end - # Rewritten from original tests to use AR def test_object_should_be_changed_if_any_attribute_is_changed pirate = Pirate.new assert !pirate.changed? @@ -62,6 +62,28 @@ class DirtyTest < Test::Unit::TestCase assert_equal Hash.new, pirate.changes end + def test_attribute_will_change! + pirate = Pirate.create!(:catchphrase => 'arr') + + pirate.catchphrase << ' matey' + assert !pirate.catchphrase_changed? + + assert pirate.catchphrase_will_change! + assert pirate.catchphrase_changed? + assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change + + pirate.catchphrase << '!' + assert pirate.catchphrase_changed? + assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change + end + + def test_association_assignment_changes_foreign_key + pirate = Pirate.create! + pirate.parrot = Parrot.create! + assert pirate.changed? + assert_equal %w(parrot_id), pirate.changed + end + def test_attribute_should_be_compared_with_type_cast topic = Topic.new assert topic.approved? @@ -74,4 +96,25 @@ class DirtyTest < Test::Unit::TestCase assert topic.approved? assert !topic.approved_changed? end + + def test_partial_update + pirate = Pirate.new(:catchphrase => 'foo') + + with_partial_updates Pirate, false do + assert_queries(2) { 2.times { pirate.save! } } + end + + with_partial_updates Pirate, true do + assert_queries(0) { 2.times { pirate.save! } } + end + end + + private + def with_partial_updates(klass, on = true) + old = klass.partial_updates? + klass.partial_updates = on + yield + ensure + klass.partial_updates = old + end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index e50cb32c65..dc9eeec281 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -82,7 +82,9 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase Task.connection.expects(:clear_query_cache).times(2) Task.cache do - Task.find(1).save! + task = Task.find(1) + task.starting = Time.now.utc + task.save! end end diff --git a/railties/configs/initializers/new_in_rails_3.rb b/railties/configs/initializers/new_in_rails_3.rb new file mode 100644 index 0000000000..0cc1993ed9 --- /dev/null +++ b/railties/configs/initializers/new_in_rails_3.rb @@ -0,0 +1,5 @@ +# These settins change the behavior of Rails 2 apps and will be defaults +# for Rails 3. You can remove this initializer when Rails 3 is released. + +# Only save the attributes that have changed since the record was loaded. +ActiveRecord::Base.partial_updates = true diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb index fc4ac8eb01..359726ff01 100644 --- a/railties/lib/rails_generator/generators/applications/app/app_generator.rb +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -61,7 +61,8 @@ class AppGenerator < Rails::Generator::Base # Initializers m.template "configs/initializers/inflections.rb", "config/initializers/inflections.rb" - m.template "configs/initializers/mime_types.rb", "config/initializers/mime_types.rb" + m.template "configs/initializers/mime_types.rb", "config/initializers/mime_types.rb" + m.template "configs/initializers/new_in_rails_3.rb", "config/initializers/new_in_rails_3.rb" # Environments m.file "environments/boot.rb", "config/boot.rb" |