activerecord-delay_touching 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +108 -0
- data/Rakefile +16 -0
- data/activerecord-delay_touching.gemspec +31 -0
- data/lib/activerecord/delay_touching.rb +106 -0
- data/lib/activerecord/delay_touching/state.rb +50 -0
- data/lib/activerecord/delay_touching/version.rb +5 -0
- data/spec/activerecord/delay_touching_spec.rb +174 -0
- data/spec/rcov_exclude_list.rb +3 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/models.rb +7 -0
- data/spec/support/schema.rb +19 -0
- metadata +192 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d2de7f476415406036017a1c6629b9d41032de7c
|
4
|
+
data.tar.gz: 5b218bc469df2764cc83397f5100aec3c519abe2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9281a41c4c3df34d928203a6fe5b7327a4519ad278219c9aa75e6e75da97aa6867662f35edd7857b50b20f11c7ed5bd340adedabf3b7f1db10637dda53e0099d
|
7
|
+
data.tar.gz: 3e214c7121a700bbc012ee1336157f440b43b60c3dda5f24bccd9d43ef791b08b65e676215b6943fbf57355e7335edd2a2dc6323cd87f1f02a5a3d29f1b6c54b
|
data/.gitignore
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.ruby-version
|
24
|
+
coverage/
|
25
|
+
results.xml
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 GoDaddy
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# Activerecord::DelayTouching
|
2
|
+
|
3
|
+
Batch up your ActiveRecord "touch" operations for better performance.
|
4
|
+
|
5
|
+
When you want to invalidate a cache in Rails, you use `touch: true`. But when
|
6
|
+
you modify a bunch of records that all `belong_to` the same owning record, that record
|
7
|
+
will be touched N times. It's incredibly slow.
|
8
|
+
|
9
|
+
With this gem, all `touch` operations are consolidated into as few database
|
10
|
+
round-trips as possible. Instead of N touches you get 1 touch.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'activerecord-delay_touching'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself:
|
23
|
+
|
24
|
+
$ gem install activerecord-delay_touching
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
The setup:
|
29
|
+
|
30
|
+
class Person < ActiveRecord::Base
|
31
|
+
has_many :pets
|
32
|
+
accepts_nested_attributes_for :pets
|
33
|
+
end
|
34
|
+
|
35
|
+
class Pet < ActiveRecord::Base
|
36
|
+
belongs_to :person, touch: true
|
37
|
+
end
|
38
|
+
|
39
|
+
Without `delay_touching`, this simple `update` in the controller calls
|
40
|
+
`@person.touch` N times, where N is the number of pets that were updated
|
41
|
+
via nested attributes. That's N-1 unnecessary round-trips to the database:
|
42
|
+
|
43
|
+
class PeopleController < ApplicationController
|
44
|
+
def update
|
45
|
+
...
|
46
|
+
#
|
47
|
+
@person.update(person_params)
|
48
|
+
...
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# SQL (0.1ms) UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.137158' WHERE "people"."id" = 1
|
53
|
+
# SQL (0.1ms) UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.138457' WHERE "people"."id" = 1
|
54
|
+
# SQL (0.1ms) UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "people"."id" = 1
|
55
|
+
|
56
|
+
With `delay_touching`, @person is touched only once:
|
57
|
+
|
58
|
+
ActiveRecord::Base.delay_touching do
|
59
|
+
@person.update(person_params)
|
60
|
+
end
|
61
|
+
|
62
|
+
# SQL (0.1ms) UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "people"."id" = 1
|
63
|
+
|
64
|
+
## Consolidates Touches Per Table
|
65
|
+
|
66
|
+
In the following example, a person gives his pet to another person. ActiveRecord
|
67
|
+
automatically touches the old person and the new person. With `delay_touching`,
|
68
|
+
this will only make a *single* round-trip to the database, setting `updated_at`
|
69
|
+
for all Person records in a single SQL UPDATE statement. Not a big deal when there are
|
70
|
+
only two touches, but when you're updating records en masse and have a cascade
|
71
|
+
of hundreds touches, it really is a big deal.
|
72
|
+
|
73
|
+
class Pet < ActiveRecord::Base
|
74
|
+
belongs_to :person, touch: true
|
75
|
+
|
76
|
+
def give(to_person)
|
77
|
+
ActiveRecord::Base.delay_touching do
|
78
|
+
self.person = to_person
|
79
|
+
save! # touches old person and new person in a single SQL UPDATE.
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
## Cascading Touches
|
85
|
+
|
86
|
+
When `delay_touch` runs through and touches everything, it captures additional
|
87
|
+
`touch` calls that might be called as side-effects. (E.g., in `after_touch`
|
88
|
+
handlers.) Then it makes a second pass, batching up those touches as well.
|
89
|
+
|
90
|
+
It keeps doing this until there are no more touches, or until the sun swallows
|
91
|
+
up the earth. Whichever comes first.
|
92
|
+
|
93
|
+
## Gotchas
|
94
|
+
|
95
|
+
Things to note:
|
96
|
+
|
97
|
+
* `after_touch` callbacks are still fired for every instance, but not until the block is exited.
|
98
|
+
And they won't happen in the same order as they would if you weren't batching up your touches.
|
99
|
+
* If you call person1.touch and then person2.touch, and they are two separate instances
|
100
|
+
with the same id, only person1's `after_touch` handler will be called.
|
101
|
+
|
102
|
+
## Contributing
|
103
|
+
|
104
|
+
1. Fork it ( https://github.com/godaddy/activerecord-delay_touching/fork )
|
105
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
106
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
107
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
108
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
Rake::Task["spec"].clear
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
7
|
+
t.fail_on_error = false
|
8
|
+
t.rspec_opts = %w[-f JUnit -o results.xml]
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Run RSpec with code coverage"
|
12
|
+
task :coverage do
|
13
|
+
ENV['COVERAGE'] = 'true'
|
14
|
+
Rake::Task["spec"].execute
|
15
|
+
end
|
16
|
+
task :default => :spec
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activerecord/delay_touching/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord-delay_touching"
|
8
|
+
spec.version = Activerecord::DelayTouching::VERSION
|
9
|
+
spec.authors = ["Brian Morearty"]
|
10
|
+
spec.email = ["brian@morearty.org"]
|
11
|
+
spec.summary = %q{Batch up your ActiveRecord "touch" operations for better performance.}
|
12
|
+
spec.description = %q{Batch up your ActiveRecord "touch" operations for better performance. ActiveRecord::Base.delay_touching do ... end. When "end" is reached, all accumulated "touch" calls will be consolidated into as few database round trips as possible.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", ">= 3.2"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "sqlite3"
|
26
|
+
spec.add_development_dependency "timecop"
|
27
|
+
spec.add_development_dependency "rspec-rails", "~> 2.0"
|
28
|
+
spec.add_development_dependency "simplecov"
|
29
|
+
spec.add_development_dependency "simplecov-rcov"
|
30
|
+
spec.add_development_dependency "yarjuf"
|
31
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require "activerecord/delay_touching/version"
|
2
|
+
require "activerecord/delay_touching/state"
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module DelayTouching
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# Override ActiveRecord::Base#touch.
|
9
|
+
def touch(name = nil)
|
10
|
+
if self.class.delay_touching? && !try(:no_touching?)
|
11
|
+
DelayTouching.add_record(self, name)
|
12
|
+
true
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# These get added as class methods to ActiveRecord::Base.
|
19
|
+
module ClassMethods
|
20
|
+
# Lets you batch up your `touch` calls for the duration of a block.
|
21
|
+
#
|
22
|
+
# ==== Examples
|
23
|
+
#
|
24
|
+
# # Touches Person.first once, not twice, when the block exits.
|
25
|
+
# ActiveRecord::Base.delay_touching do
|
26
|
+
# Person.first.touch
|
27
|
+
# Person.first.touch
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
def delay_touching(&block)
|
31
|
+
DelayTouching.call &block
|
32
|
+
end
|
33
|
+
|
34
|
+
# Are we currently executing in a delay_touching block?
|
35
|
+
def delay_touching?
|
36
|
+
DelayTouching.state.nesting > 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.state
|
41
|
+
Thread.current[:delay_touching_state] ||= State.new
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
delegate :add_record, to: :state
|
46
|
+
end
|
47
|
+
|
48
|
+
# Start delaying all touches. When done, apply them. (Unless nested.)
|
49
|
+
def self.call
|
50
|
+
state.nesting += 1
|
51
|
+
begin
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
apply if state.nesting == 1
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
# Decrement nesting even if `apply` raised an error.
|
58
|
+
state.nesting -= 1
|
59
|
+
end
|
60
|
+
|
61
|
+
# Apply the touches that were delayed.
|
62
|
+
def self.apply
|
63
|
+
begin
|
64
|
+
ActiveRecord::Base.transaction do
|
65
|
+
state.records_by_attrs_and_class.each do |attr, classes_and_records|
|
66
|
+
classes_and_records.each do |klass, records|
|
67
|
+
touch_records attr, klass, records
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end while state.more_records?
|
72
|
+
ensure
|
73
|
+
state.clear_records
|
74
|
+
end
|
75
|
+
|
76
|
+
# Touch the specified records--non-empty set of instances of the same class.
|
77
|
+
def self.touch_records(attr, klass, records)
|
78
|
+
attributes = records.first.send(:timestamp_attributes_for_update_in_model)
|
79
|
+
attributes << attr if attr
|
80
|
+
|
81
|
+
if attributes.present?
|
82
|
+
current_time = records.first.send(:current_time_from_proper_timezone)
|
83
|
+
changes = {}
|
84
|
+
|
85
|
+
attributes.each do |column|
|
86
|
+
column = column.to_s
|
87
|
+
changes[column] = current_time
|
88
|
+
records.each do |record|
|
89
|
+
record.instance_eval do
|
90
|
+
write_attribute column, current_time
|
91
|
+
@changed_attributes.except!(*changes.keys)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
klass.unscoped.where(klass.primary_key => records).update_all(changes)
|
97
|
+
end
|
98
|
+
state.updated attr, records
|
99
|
+
records.each { |record| record.run_callbacks(:touch) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
ActiveRecord::Base.class_eval do
|
105
|
+
include ActiveRecord::DelayTouching
|
106
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "activerecord/delay_touching/version"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module DelayTouching
|
5
|
+
|
6
|
+
# Tracking of the touch state. This class has no class-level data, so you can
|
7
|
+
# store per-thread instances in thread-local variables.
|
8
|
+
class State
|
9
|
+
attr_accessor :nesting
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@records = Hash.new { Set.new }
|
13
|
+
@already_updated_records = Hash.new { Set.new }
|
14
|
+
@nesting = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def updated(attr, records)
|
18
|
+
@records[attr].subtract records
|
19
|
+
@records.delete attr if @records[attr].empty?
|
20
|
+
@already_updated_records[attr] += records
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the records grouped by the attributes that were touched, and by class:
|
24
|
+
# [
|
25
|
+
# [
|
26
|
+
# nil, { Person => [ person1, person2 ], Pet => [ pet1 ] }
|
27
|
+
# ],
|
28
|
+
# [
|
29
|
+
# :neutered_at, { Pet => [ pet1 ] }
|
30
|
+
# ],
|
31
|
+
# ]
|
32
|
+
def records_by_attrs_and_class
|
33
|
+
@records.map { |attrs, records| [attrs, records.group_by(&:class)] }
|
34
|
+
end
|
35
|
+
|
36
|
+
def more_records?
|
37
|
+
@records.present?
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_record(record, column)
|
41
|
+
@records[column] += [ record ] unless @already_updated_records[column].include?(record)
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear_records
|
45
|
+
@records.clear
|
46
|
+
@already_updated_records.clear
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Activerecord::DelayTouching do
|
4
|
+
let(:person) { Person.create name: "Rosey" }
|
5
|
+
let(:pet1) { Pet.create(name: "Bones") }
|
6
|
+
let(:pet2) { Pet.create(name: "Ema") }
|
7
|
+
|
8
|
+
it 'has a version number' do
|
9
|
+
expect(Activerecord::DelayTouching::VERSION).not_to be nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'touch returns true' do
|
13
|
+
ActiveRecord::Base.delay_touching do
|
14
|
+
expect(person.touch).to eq(true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'consolidates touches on a single record' do
|
19
|
+
expect_updates ["people"] do
|
20
|
+
ActiveRecord::Base.delay_touching do
|
21
|
+
person.touch
|
22
|
+
person.touch
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'sets updated_at on the in-memory instance when it eventually touches the record' do
|
28
|
+
original_time = new_time = nil
|
29
|
+
|
30
|
+
Timecop.freeze(2014, 7, 4, 12, 0, 0) do
|
31
|
+
original_time = Time.current
|
32
|
+
person.touch
|
33
|
+
end
|
34
|
+
|
35
|
+
Timecop.freeze(2014, 7, 10, 12, 0, 0) do
|
36
|
+
new_time = Time.current
|
37
|
+
ActiveRecord::Base.delay_touching do
|
38
|
+
person.touch
|
39
|
+
expect(person.updated_at).to eq(original_time)
|
40
|
+
expect(person.changed?).to be_falsey
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
expect(person.updated_at).to eq(new_time)
|
45
|
+
expect(person.changed?).to be_falsey
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'does not mark the instance as changed when touch is called' do
|
49
|
+
ActiveRecord::Base.delay_touching do
|
50
|
+
person.touch
|
51
|
+
expect(person).not_to be_changed
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'consolidates touches for all instances in a single table' do
|
56
|
+
expect_updates ["pets"] do
|
57
|
+
ActiveRecord::Base.delay_touching do
|
58
|
+
pet1.touch
|
59
|
+
pet2.touch
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'does nothing if no_touching is on' do
|
65
|
+
if ActiveRecord::Base.respond_to?(:no_touching)
|
66
|
+
expect_updates [] do
|
67
|
+
ActiveRecord::Base.no_touching do
|
68
|
+
ActiveRecord::Base.delay_touching do
|
69
|
+
person.touch
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'only applies touches for which no_touching is off' do
|
77
|
+
if Person.respond_to?(:no_touching)
|
78
|
+
expect_updates ["pets"] do
|
79
|
+
Person.no_touching do
|
80
|
+
ActiveRecord::Base.delay_touching do
|
81
|
+
person.touch
|
82
|
+
pet1.touch
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'does not apply nested touches if no_touching was turned on inside delay_touching' do
|
90
|
+
if ActiveRecord::Base.respond_to?(:no_touching)
|
91
|
+
expect_updates [ "people" ] do
|
92
|
+
ActiveRecord::Base.delay_touching do
|
93
|
+
person.touch
|
94
|
+
ActiveRecord::Base.no_touching do
|
95
|
+
pet1.touch
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'can update nonstandard columns' do
|
103
|
+
expect_updates [ "pets" => [ "updated_at", "neutered_at" ] ] do
|
104
|
+
ActiveRecord::Base.delay_touching do
|
105
|
+
pet1.touch :neutered_at
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'splits up nonstandard column touches and standard column touches' do
|
111
|
+
expect_updates [ { "pets" => [ "updated_at", "neutered_at" ] }, { "pets" => [ "updated_at" ] } ] do
|
112
|
+
ActiveRecord::Base.delay_touching do
|
113
|
+
pet1.touch :neutered_at
|
114
|
+
pet2.touch
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'can update multiple nonstandard columns of a single record in different calls to touch' do
|
120
|
+
expect_updates [ { "pets" => [ "updated_at", "neutered_at" ] }, { "pets" => [ "updated_at", "fed_at" ] } ] do
|
121
|
+
ActiveRecord::Base.delay_touching do
|
122
|
+
pet1.touch :neutered_at
|
123
|
+
pet1.touch :fed_at
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'touch: true' do
|
129
|
+
before do
|
130
|
+
person.pets << pet1
|
131
|
+
person.pets << pet2
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'consolidates touch: true touches' do
|
135
|
+
expect_updates [ "pets", "people" ] do
|
136
|
+
ActiveRecord::Base.delay_touching do
|
137
|
+
pet1.touch
|
138
|
+
pet2.touch
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'does not touch the owning record via touch: true if it was already touched explicitly' do
|
144
|
+
expect_updates [ "pets", "people" ] do
|
145
|
+
ActiveRecord::Base.delay_touching do
|
146
|
+
person.touch
|
147
|
+
pet1.touch
|
148
|
+
pet2.touch
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def expect_updates(tables)
|
155
|
+
expected_sql = tables.map do |entry|
|
156
|
+
if entry.kind_of?(Hash)
|
157
|
+
entry.map do |table, columns|
|
158
|
+
Regexp.new(%Q{UPDATE "#{table}" SET #{columns.map { |column| %Q{"#{column}" =.+} }.join(", ") } })
|
159
|
+
end
|
160
|
+
else
|
161
|
+
Regexp.new(%Q{UPDATE "#{entry}" SET "updated_at" = })
|
162
|
+
end
|
163
|
+
end.flatten
|
164
|
+
expect(ActiveRecord::Base.connection).to receive(:update).exactly(expected_sql.length).times do |stmt, _, _|
|
165
|
+
index = expected_sql.index { |sql| stmt.to_sql =~ sql}
|
166
|
+
expect(index).to be, "An unexpected touch occurred: #{stmt.to_sql}"
|
167
|
+
expected_sql.delete_at(index)
|
168
|
+
end
|
169
|
+
|
170
|
+
yield
|
171
|
+
|
172
|
+
expect(expected_sql).to be_empty, "Some of the expected updates were not executed."
|
173
|
+
end
|
174
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yarjuf'
|
2
|
+
|
3
|
+
if ENV["COVERAGE"]
|
4
|
+
require_relative 'rcov_exclude_list.rb'
|
5
|
+
exlist = Dir.glob(@exclude_list)
|
6
|
+
require 'simplecov'
|
7
|
+
require 'simplecov-rcov'
|
8
|
+
SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
|
9
|
+
SimpleCov.start do
|
10
|
+
exlist.each do |p|
|
11
|
+
add_filter p
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
17
|
+
require 'active_record'
|
18
|
+
require 'activerecord/delay_touching'
|
19
|
+
require 'timecop'
|
20
|
+
|
21
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
22
|
+
|
23
|
+
load File.dirname(__FILE__) + '/support/schema.rb'
|
24
|
+
require File.dirname(__FILE__) + '/support/models.rb'
|
25
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
self.verbose = false
|
3
|
+
|
4
|
+
create_table :people, :force => true do |t|
|
5
|
+
t.string :name
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table :pets, :force => true do |t|
|
11
|
+
t.string :name
|
12
|
+
t.integer :person_id
|
13
|
+
t.datetime :neutered_at
|
14
|
+
t.datetime :fed_at
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-delay_touching
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Morearty
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov-rcov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yarjuf
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Batch up your ActiveRecord "touch" operations for better performance.
|
140
|
+
ActiveRecord::Base.delay_touching do ... end. When "end" is reached, all accumulated
|
141
|
+
"touch" calls will be consolidated into as few database round trips as possible.
|
142
|
+
email:
|
143
|
+
- brian@morearty.org
|
144
|
+
executables: []
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files: []
|
147
|
+
files:
|
148
|
+
- ".gitignore"
|
149
|
+
- ".rspec"
|
150
|
+
- Gemfile
|
151
|
+
- LICENSE.txt
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- activerecord-delay_touching.gemspec
|
155
|
+
- lib/activerecord/delay_touching.rb
|
156
|
+
- lib/activerecord/delay_touching/state.rb
|
157
|
+
- lib/activerecord/delay_touching/version.rb
|
158
|
+
- spec/activerecord/delay_touching_spec.rb
|
159
|
+
- spec/rcov_exclude_list.rb
|
160
|
+
- spec/spec_helper.rb
|
161
|
+
- spec/support/models.rb
|
162
|
+
- spec/support/schema.rb
|
163
|
+
homepage: ''
|
164
|
+
licenses:
|
165
|
+
- MIT
|
166
|
+
metadata: {}
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 2.2.2
|
184
|
+
signing_key:
|
185
|
+
specification_version: 4
|
186
|
+
summary: Batch up your ActiveRecord "touch" operations for better performance.
|
187
|
+
test_files:
|
188
|
+
- spec/activerecord/delay_touching_spec.rb
|
189
|
+
- spec/rcov_exclude_list.rb
|
190
|
+
- spec/spec_helper.rb
|
191
|
+
- spec/support/models.rb
|
192
|
+
- spec/support/schema.rb
|