From b32ba367f584a6298fb8b7eef97be15388b5bd87 Mon Sep 17 00:00:00 2001 From: Damien Mathieu <42@dmathieu.com> Date: Tue, 5 Nov 2013 13:44:16 +0100 Subject: [PATCH] add #no_touching on ActiveRecord models --- activerecord/CHANGELOG.md | 10 ++++ activerecord/lib/active_record.rb | 1 + activerecord/lib/active_record/base.rb | 1 + activerecord/lib/active_record/no_touching.rb | 52 +++++++++++++++++++ activerecord/test/cases/timestamp_test.rb | 48 +++++++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 activerecord/lib/active_record/no_touching.rb diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 524a048c7cf1a..fa00d6db1c761 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,13 @@ +* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on models. + + Examples: + + Post.no_touching do + Post.first.touch + end + + *Sam Stephenson*, *Damien Mathieu* + * Prevent the counter cache from being decremented twice when destroying a record on a has_many :through association. diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 7a2c5c8bf2396..cbac2ef3c6b7f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -45,6 +45,7 @@ module ActiveRecord autoload :Migrator, 'active_record/migration' autoload :ModelSchema autoload :NestedAttributes + autoload :NoTouching autoload :Persistence autoload :QueryCache autoload :Querying diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 69a9eabefbf7d..e05e22ebb0cfb 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -295,6 +295,7 @@ class Base extend Delegation::DelegateCache include Persistence + include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb new file mode 100644 index 0000000000000..dbf4564ae58c9 --- /dev/null +++ b/activerecord/lib/active_record/no_touching.rb @@ -0,0 +1,52 @@ +module ActiveRecord + # = Active Record No Touching + module NoTouching + extend ActiveSupport::Concern + + module ClassMethods + # Lets you selectively disable calls to `touch` for the + # duration of a block. + # + # ==== Examples + # ActiveRecord::Base.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # does nothing + # end + # + # Project.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # works, but does not touch the associated project + # end + # + def no_touching(&block) + NoTouching.apply_to(self, &block) + end + end + + class << self + def apply_to(klass) #:nodoc: + klasses.push(klass) + yield + ensure + klasses.pop + end + + def applied_to?(klass) #:nodoc: + klasses.any? { |k| k >= klass } + end + + private + def klasses + Thread.current[:no_touching_classes] ||= [] + end + end + + def no_touching? + NoTouching.applied_to?(self.class) + end + + def touch(*) + super unless no_touching? + end + end +end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index ff1b01556d41e..8c45f2a3f8b73 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,6 +11,7 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first + @owner = Owner.first @developer.update_columns(updated_at: Time.now.prev_month) @previously_updated_at = @developer.updated_at end @@ -92,6 +93,53 @@ def test_touching_a_record_without_timestamps_is_unexceptional assert_nothing_raised { Car.first.touch } end + def test_touching_a_no_touching_object + Developer.no_touching do + assert @developer.no_touching? + assert !@owner.no_touching? + @developer.touch + end + + assert !@developer.no_touching? + assert !@owner.no_touching? + assert_equal @previously_updated_at, @developer.updated_at + end + + def test_touching_related_objects + @owner = Owner.first + @previously_updated_at = @owner.updated_at + + Owner.no_touching do + @owner.pets.first.touch + end + + assert_equal @previously_updated_at, @owner.updated_at + end + + def test_global_no_touching + ActiveRecord::Base.no_touching do + assert @developer.no_touching? + assert @owner.no_touching? + @developer.touch + end + + assert !@developer.no_touching? + assert !@owner.no_touching? + assert_equal @previously_updated_at, @developer.updated_at + end + + def test_no_touching_threadsafe + Thread.new do + Developer.no_touching do + assert @developer.no_touching? + + sleep(1) + end + end + + assert !@developer.no_touching? + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at pet = Pet.first owner = pet.owner