jit_preloader 0.1.0 → 0.2.0
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 +4 -4
- data/Gemfile.lock +17 -16
- data/README.md +2 -2
- data/jit_preloader.gemspec +1 -1
- data/lib/jit_preloader.rb +6 -2
- data/lib/jit_preloader/active_record/associations/preloader/association.rb +71 -0
- data/lib/jit_preloader/active_record/associations/preloader/association.rb~ +34 -0
- data/lib/jit_preloader/active_record/base.rb +4 -0
- data/lib/jit_preloader/version.rb +1 -1
- data/spec/lib/jit_preloader/preloader_spec.rb +45 -0
- data/spec/support/database.rb +2 -1
- data/spec/support/models.rb +8 -0
- metadata +9 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d42276971ec067b0aafaf2252e62e7a8f974525
|
|
4
|
+
data.tar.gz: 5c2cc5de28be5ebedc033d704971937dcd9904d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71cfe85e0ca02afa0632b7efcf4991ca3778fa0870bb4773c1f789ff18bb21e9d50d08b5d2bffb9590c660480bc2e57061fce6e97d5553d586b9bdfd2108a5e6
|
|
7
|
+
data.tar.gz: 23828f4c2da51cbfb4e8e5f4026472875d29119005045d4d720908380811b0662aa0ca013e38af3a52accc8d08233ac358a2d92437184426645209a558479125
|
data/Gemfile.lock
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
jit_preloader (0.0
|
|
4
|
+
jit_preloader (0.2.0)
|
|
5
5
|
activerecord (> 4.2, < 6)
|
|
6
6
|
activesupport
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activemodel (5.
|
|
12
|
-
activesupport (= 5.
|
|
13
|
-
activerecord (5.
|
|
14
|
-
activemodel (= 5.
|
|
15
|
-
activesupport (= 5.
|
|
16
|
-
arel (
|
|
17
|
-
activesupport (5.
|
|
11
|
+
activemodel (5.2.2)
|
|
12
|
+
activesupport (= 5.2.2)
|
|
13
|
+
activerecord (5.2.2)
|
|
14
|
+
activemodel (= 5.2.2)
|
|
15
|
+
activesupport (= 5.2.2)
|
|
16
|
+
arel (>= 9.0)
|
|
17
|
+
activesupport (5.2.2)
|
|
18
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
19
|
-
i18n (
|
|
19
|
+
i18n (>= 0.7, < 2)
|
|
20
20
|
minitest (~> 5.1)
|
|
21
21
|
tzinfo (~> 1.1)
|
|
22
|
-
arel (
|
|
22
|
+
arel (9.0.0)
|
|
23
23
|
byebug (9.0.6)
|
|
24
|
-
concurrent-ruby (1.
|
|
24
|
+
concurrent-ruby (1.1.4)
|
|
25
25
|
database_cleaner (1.5.3)
|
|
26
26
|
diff-lcs (1.2.5)
|
|
27
|
-
i18n (
|
|
28
|
-
|
|
27
|
+
i18n (1.6.0)
|
|
28
|
+
concurrent-ruby (~> 1.0)
|
|
29
|
+
minitest (5.11.3)
|
|
29
30
|
rake (10.5.0)
|
|
30
31
|
rspec (3.5.0)
|
|
31
32
|
rspec-core (~> 3.5.0)
|
|
@@ -42,14 +43,14 @@ GEM
|
|
|
42
43
|
rspec-support (3.5.0)
|
|
43
44
|
sqlite3 (1.3.12)
|
|
44
45
|
thread_safe (0.3.6)
|
|
45
|
-
tzinfo (1.2.
|
|
46
|
+
tzinfo (1.2.5)
|
|
46
47
|
thread_safe (~> 0.1)
|
|
47
48
|
|
|
48
49
|
PLATFORMS
|
|
49
50
|
ruby
|
|
50
51
|
|
|
51
52
|
DEPENDENCIES
|
|
52
|
-
bundler
|
|
53
|
+
bundler
|
|
53
54
|
byebug
|
|
54
55
|
database_cleaner
|
|
55
56
|
jit_preloader!
|
|
@@ -58,4 +59,4 @@ DEPENDENCIES
|
|
|
58
59
|
sqlite3
|
|
59
60
|
|
|
60
61
|
BUNDLED WITH
|
|
61
|
-
|
|
62
|
+
2.0.1
|
data/README.md
CHANGED
|
@@ -157,7 +157,7 @@ end
|
|
|
157
157
|
# ...
|
|
158
158
|
|
|
159
159
|
#new
|
|
160
|
-
class Contact < ActiveRecord::
|
|
160
|
+
class Contact < ActiveRecord::Base
|
|
161
161
|
has_many :addresses
|
|
162
162
|
has_many_aggregate :addresses, :max_street_length, :maximum, "LENGTH(street)", default: nil
|
|
163
163
|
has_many_aggregate :addresses, :count_all, :count, "*"
|
|
@@ -165,7 +165,7 @@ end
|
|
|
165
165
|
|
|
166
166
|
Contact.jit_preload.each do |contact|
|
|
167
167
|
contact.addresses_max_street_length
|
|
168
|
-
contact.
|
|
168
|
+
contact.addresses_count_all
|
|
169
169
|
end
|
|
170
170
|
# SELECT * FROM contacts
|
|
171
171
|
# SELECT contact_id, MAX(LENGTH(street)) FROM addresses WHERE contact_id IN (1, 2, 3, ...) GROUP BY contact_id
|
data/jit_preloader.gemspec
CHANGED
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.add_dependency "activerecord", "> 4.2", "< 6"
|
|
22
22
|
spec.add_dependency "activesupport"
|
|
23
23
|
|
|
24
|
-
spec.add_development_dependency "bundler"
|
|
24
|
+
spec.add_development_dependency "bundler"
|
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
|
26
26
|
spec.add_development_dependency "rspec"
|
|
27
27
|
spec.add_development_dependency "database_cleaner"
|
data/lib/jit_preloader.rb
CHANGED
|
@@ -8,8 +8,12 @@ require 'jit_preloader/active_record/base'
|
|
|
8
8
|
require 'jit_preloader/active_record/relation'
|
|
9
9
|
require 'jit_preloader/active_record/associations/collection_association'
|
|
10
10
|
require 'jit_preloader/active_record/associations/singular_association'
|
|
11
|
-
|
|
12
|
-
require 'jit_preloader/active_record/associations/preloader/
|
|
11
|
+
if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.2.2")
|
|
12
|
+
require 'jit_preloader/active_record/associations/preloader/collection_association'
|
|
13
|
+
require 'jit_preloader/active_record/associations/preloader/singular_association'
|
|
14
|
+
else
|
|
15
|
+
require 'jit_preloader/active_record/associations/preloader/association'
|
|
16
|
+
end
|
|
13
17
|
require 'jit_preloader/preloader'
|
|
14
18
|
|
|
15
19
|
module JitPreloader
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module JitPreloader
|
|
2
|
+
module PreloaderAssociation
|
|
3
|
+
|
|
4
|
+
# A monkey patch to ActiveRecord. The old method looked like the snippet
|
|
5
|
+
# below. Our changes here are that we remove records that are already
|
|
6
|
+
# part of the target, then attach all of the records to a new jit preloader.
|
|
7
|
+
#
|
|
8
|
+
# def run(preloader)
|
|
9
|
+
# records = load_records do |record|
|
|
10
|
+
# owner = owners_by_key[convert_key(record[association_key_name])]
|
|
11
|
+
# association = owner.association(reflection.name)
|
|
12
|
+
# association.set_inverse_instance(record)
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
# owners.each do |owner|
|
|
16
|
+
# associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
def run(preloader)
|
|
21
|
+
all_records = []
|
|
22
|
+
records = load_records do |record|
|
|
23
|
+
owner = owners_by_key[convert_key(record[association_key_name])]
|
|
24
|
+
association = owner.association(reflection.name)
|
|
25
|
+
association.set_inverse_instance(record)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
owners.each do |owner|
|
|
29
|
+
owned_records = records[convert_key(owner[owner_key_name])] || []
|
|
30
|
+
all_records.concat(Array(owned_records)) if owner.jit_preloader || JitPreloader.globally_enabled?
|
|
31
|
+
associate_records_to_owner(owner, owned_records)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
JitPreloader::Preloader.attach(all_records) if all_records.any?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Original method:
|
|
38
|
+
# def associate_records_to_owner(owner, records)
|
|
39
|
+
# association = owner.association(reflection.name)
|
|
40
|
+
# association.loaded!
|
|
41
|
+
# if reflection.collection?
|
|
42
|
+
# association.target.concat(records)
|
|
43
|
+
# else
|
|
44
|
+
# association.target = records.first unless records.empty?
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
def associate_records_to_owner(owner, records)
|
|
48
|
+
association = owner.association(reflection.name)
|
|
49
|
+
association.loaded!
|
|
50
|
+
|
|
51
|
+
if reflection.collection?
|
|
52
|
+
# It is possible that some of the records are loaded already.
|
|
53
|
+
# We don't want to duplicate them, but we also want to preserve
|
|
54
|
+
# the original copy so that we don't blow away in-memory changes.
|
|
55
|
+
new_records = association.target.any? ? records - association.target : records
|
|
56
|
+
association.target.concat(new_records)
|
|
57
|
+
else
|
|
58
|
+
association.target ||= records.first unless records.empty?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_scope
|
|
64
|
+
super.tap do |scope|
|
|
65
|
+
scope.jit_preload! if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
ActiveRecord::Associations::Preloader::Association.prepend(JitPreloader::PreloaderAssociation)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class ActiveRecord::Associations::Preloader::Association
|
|
2
|
+
private
|
|
3
|
+
# A monkey patch to ActiveRecord. The old method looked like the snippet
|
|
4
|
+
# below. Our changes here are that we remove records that are already
|
|
5
|
+
# part of the target, then attach all of the records to a new jit preloader.
|
|
6
|
+
#
|
|
7
|
+
# def preload(preloader)
|
|
8
|
+
# associated_records_by_owner(preloader).each do |owner, records|
|
|
9
|
+
# association = owner.association(reflection.name)
|
|
10
|
+
# association.loaded!
|
|
11
|
+
# association.target.concat(records)
|
|
12
|
+
# records.each { |record| association.set_inverse_instance(record) }
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
|
|
16
|
+
def preload(preloader)
|
|
17
|
+
return unless (reflection.scope.nil? || reflection.scope.arity == 0) && klass.ancestors.include?(ActiveRecord::Base)
|
|
18
|
+
all_records = []
|
|
19
|
+
associated_records_by_owner(preloader).each do |owner, records|
|
|
20
|
+
association = owner.association(reflection.name)
|
|
21
|
+
association.loaded!
|
|
22
|
+
# It is possible that some of the records are loaded already.
|
|
23
|
+
# We don't want to duplicate them, but we also want to preserve
|
|
24
|
+
# the original copy so that we don't blow away in-memory changes.
|
|
25
|
+
new_records = association.target.any? ? records - association.target : records
|
|
26
|
+
|
|
27
|
+
association.target.concat(new_records)
|
|
28
|
+
new_records.each { |record| association.set_inverse_instance(record) }
|
|
29
|
+
|
|
30
|
+
all_records.concat(records) if owner.jit_preloader || JitPreloader.globally_enabled?
|
|
31
|
+
end
|
|
32
|
+
JitPreloader::Preloader.attach(all_records) if all_records.any?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -43,6 +43,10 @@ module JitPreloadExtension
|
|
|
43
43
|
|
|
44
44
|
conditions[reflection.foreign_key] = primary_ids
|
|
45
45
|
|
|
46
|
+
if reflection.type.present?
|
|
47
|
+
conditions[reflection.type] = self.class.name
|
|
48
|
+
end
|
|
49
|
+
|
|
46
50
|
preloaded_data = Hash[association_scope
|
|
47
51
|
.where(conditions)
|
|
48
52
|
.group(reflection.foreign_key)
|
|
@@ -32,6 +32,15 @@ RSpec.describe JitPreloader::Preloader do
|
|
|
32
32
|
)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
let!(:contact_owner) do
|
|
36
|
+
contact3.contact_owner_id = contact1.id
|
|
37
|
+
contact3.contact_owner_type = "Address"
|
|
38
|
+
contact3.save!
|
|
39
|
+
ContactOwner.create(
|
|
40
|
+
contacts: [contact1, contact2],
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
35
44
|
let(:canada) { Country.create(name: "Canada") }
|
|
36
45
|
let(:usa) { Country.create(name: "U.S.A") }
|
|
37
46
|
|
|
@@ -40,6 +49,42 @@ RSpec.describe JitPreloader::Preloader do
|
|
|
40
49
|
->(event, data){ source_map[data[:source]] << data[:association] }
|
|
41
50
|
end
|
|
42
51
|
|
|
52
|
+
context "when preloading an aggregate as polymorphic" do
|
|
53
|
+
let(:contact_owner_counts) { [2] }
|
|
54
|
+
|
|
55
|
+
context "without jit preload" do
|
|
56
|
+
it "generates N+1 query notifications for each one" do
|
|
57
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
|
58
|
+
ContactOwner.all.each_with_index do |c, i|
|
|
59
|
+
expect(c.contacts_count).to eql contact_owner_counts[i]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
contact_owner_queries = [contact_owner].product([["contacts.count"]])
|
|
64
|
+
expect(source_map).to eql(Hash[contact_owner_queries])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context "with jit_preload" do
|
|
69
|
+
|
|
70
|
+
it "does NOT generate N+1 query notifications" do
|
|
71
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
|
72
|
+
ContactOwner.jit_preload.each_with_index do |c, i|
|
|
73
|
+
expect(c.contacts_count).to eql contact_owner_counts[i]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
expect(source_map).to eql({})
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "can handle queries" do
|
|
81
|
+
ContactOwner.jit_preload.each_with_index do |c, i|
|
|
82
|
+
expect(c.contacts_count).to eql contact_owner_counts[i]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
43
88
|
context "when preloading an aggregate" do
|
|
44
89
|
let(:addresses_counts) { [3, 0, 2] }
|
|
45
90
|
let(:phone_number_counts) { [2, 0, 1] }
|
data/spec/support/database.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
class Database
|
|
2
2
|
def self.tables
|
|
3
3
|
[
|
|
4
|
-
"CREATE TABLE contacts (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
|
4
|
+
"CREATE TABLE contacts (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255), contact_owner_id INTEGER, contact_owner_type VARCHAR(255))",
|
|
5
|
+
"CREATE TABLE contact_owners (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
|
5
6
|
"CREATE TABLE addresses (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, country_id INTEGER NOT NULL, street VARCHAR(255))",
|
|
6
7
|
"CREATE TABLE email_addresses (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, address VARCHAR(255))",
|
|
7
8
|
"CREATE TABLE phone_numbers (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, phone VARCHAR(10))",
|
data/spec/support/models.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
class Contact < ActiveRecord::Base
|
|
2
|
+
belongs_to :contact_owner, polymorphic: true
|
|
3
|
+
|
|
2
4
|
has_many :addresses
|
|
3
5
|
has_many :phone_numbers
|
|
4
6
|
has_one :email_address
|
|
5
7
|
|
|
8
|
+
|
|
6
9
|
has_many_aggregate :addresses, :max_street_length, :maximum, "LENGTH(street)"
|
|
7
10
|
has_many_aggregate :phone_numbers, :count, :count, "id"
|
|
8
11
|
has_many_aggregate :addresses, :count, :count, "*"
|
|
@@ -24,3 +27,8 @@ end
|
|
|
24
27
|
class Country < ActiveRecord::Base
|
|
25
28
|
has_many :addresses
|
|
26
29
|
end
|
|
30
|
+
|
|
31
|
+
class ContactOwner < ActiveRecord::Base
|
|
32
|
+
has_many :contacts, as: :contact_owner
|
|
33
|
+
has_many_aggregate :contacts, :count, :count, "*"
|
|
34
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jit_preloader
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kyle d'Oliveira
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2019-03-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -48,16 +48,16 @@ dependencies:
|
|
|
48
48
|
name: bundler
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- - "
|
|
51
|
+
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
53
|
+
version: '0'
|
|
54
54
|
type: :development
|
|
55
55
|
prerelease: false
|
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
|
-
- - "
|
|
58
|
+
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
60
|
+
version: '0'
|
|
61
61
|
- !ruby/object:Gem::Dependency
|
|
62
62
|
name: rake
|
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -147,6 +147,8 @@ files:
|
|
|
147
147
|
- jit_preloader.gemspec
|
|
148
148
|
- lib/jit_preloader.rb
|
|
149
149
|
- lib/jit_preloader/active_record/associations/collection_association.rb
|
|
150
|
+
- lib/jit_preloader/active_record/associations/preloader/association.rb
|
|
151
|
+
- lib/jit_preloader/active_record/associations/preloader/association.rb~
|
|
150
152
|
- lib/jit_preloader/active_record/associations/preloader/collection_association.rb
|
|
151
153
|
- lib/jit_preloader/active_record/associations/preloader/singular_association.rb
|
|
152
154
|
- lib/jit_preloader/active_record/associations/singular_association.rb
|
|
@@ -178,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
178
180
|
version: '0'
|
|
179
181
|
requirements: []
|
|
180
182
|
rubyforge_project:
|
|
181
|
-
rubygems_version: 2.
|
|
183
|
+
rubygems_version: 2.6.14
|
|
182
184
|
signing_key:
|
|
183
185
|
specification_version: 4
|
|
184
186
|
summary: Tool to understand N+1 queries and to remove them
|