jit_preloader 0.2.2 → 1.0.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/.gitignore +1 -1
- data/README.md +39 -2
- data/jit_preloader.gemspec +3 -2
- data/lib/jit_preloader.rb +5 -4
- data/lib/jit_preloader/active_record/associations/preloader/{association.rb → ar5_association.rb} +6 -13
- data/lib/jit_preloader/active_record/associations/preloader/ar6_association.rb +55 -0
- data/lib/jit_preloader/active_record/base.rb +74 -3
- data/lib/jit_preloader/version.rb +1 -1
- data/spec/lib/jit_preloader/active_record/base_spec.rb +94 -0
- data/spec/lib/jit_preloader/preloader_spec.rb +73 -1
- data/spec/support/database.rb +3 -1
- data/spec/support/models.rb +43 -0
- metadata +27 -11
- data/Gemfile.lock +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a3ebb3488bb221f516f32ebd2fbbedcee866b57d3ccc3c171f3843b9cc0af45
|
4
|
+
data.tar.gz: 7bcaa0e57a6ee5541ca0fac7013f4188113f3425675df4ad29cab7cba3c8a540
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d696d0f3465be1aa319ae86c335e087b967847fefcd49a926e27c1cc24681be824ecc2a95a6365a129ae668f2abb341d570841f242cbb4b47295f974a3bf71e
|
7
|
+
data.tar.gz: 5ac6f866cc9ee0fbab43e36d8bd2134e42782e6a6e9e4e635bd9b15e5951a02ed2d1d86b003afdbf5aeac736ffd3a23aadc7e9f75db1679cfd0768e132e2d5b4
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -173,6 +173,43 @@ end
|
|
173
173
|
|
174
174
|
```
|
175
175
|
|
176
|
+
### Preloading a subset of an association
|
177
|
+
|
178
|
+
There are often times when you want to preload a subset of an association, or change how the SQL statement is generated. For example, if a `Contact` model has
|
179
|
+
an `addresses` association, you may want to be able to get all of the addresses that belong to a specific country without introducing an N+1 query.
|
180
|
+
This is a method `preload_scoped_relation` that is available that can handle this for you.
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
#old
|
184
|
+
class Contact < ActiveRecord::Base
|
185
|
+
has_many :addresses
|
186
|
+
has_many :usa_addresses, ->{ where(country: Country.find_by_name("USA")) }
|
187
|
+
end
|
188
|
+
|
189
|
+
Contact.jit_preload.all.each do |contact|
|
190
|
+
# This will preload the association as expected, but it must be defined as an association in advance
|
191
|
+
contact.usa_addresses
|
192
|
+
|
193
|
+
# This will preload as the entire addresses association, and filters it in memory
|
194
|
+
contact.addresses.select{|address| address.country == Country.find_by_name("USA") }
|
195
|
+
|
196
|
+
# This is an N+1 query
|
197
|
+
contact.addresses.where(country: Country.find_by_name("USA"))
|
198
|
+
end
|
199
|
+
|
200
|
+
# New
|
201
|
+
Contact.jit_preload.all.each do |contact|
|
202
|
+
contact.preload_scoped_relation(
|
203
|
+
name: "USA Addresses",
|
204
|
+
base_association: :addresses,
|
205
|
+
preload_scope: Address.where(country: Country.find_by_name("USA"))
|
206
|
+
)
|
207
|
+
end
|
208
|
+
# SELECT * FROM contacts
|
209
|
+
# SELECT * FROM countries WHERE name = "USA" LIMIT 1
|
210
|
+
# SELECT "addresses".* FROM "addresses" WHERE "addresses"."country_id" = 10 AND "addresses"."contact_id" IN (1, 2, 3, ...)
|
211
|
+
```
|
212
|
+
|
176
213
|
### Jit preloading globally across your application
|
177
214
|
|
178
215
|
The JitPreloader can be globally enabled, in which case most N+1 queries in your app should just disappear. It is off by default.
|
@@ -201,7 +238,7 @@ This is mostly a magic bullet, but it doesn't solve all database-related problem
|
|
201
238
|
```ruby
|
202
239
|
Contact.all.each do |contact|
|
203
240
|
contact.emails.reload # Reloading the association
|
204
|
-
contact.addresses.where(billing: true).to_a # Querying the association
|
241
|
+
contact.addresses.where(billing: true).to_a # Querying the association (Use: preload_scoped_relation to avoid these)
|
205
242
|
end
|
206
243
|
```
|
207
244
|
|
@@ -215,7 +252,7 @@ end
|
|
215
252
|
|
216
253
|
## Contributing
|
217
254
|
|
218
|
-
1. Fork it ( https://github.com/
|
255
|
+
1. Fork it ( https://github.com/clio/jit_preloader/fork )
|
219
256
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
220
257
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
221
258
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/jit_preloader.gemspec
CHANGED
@@ -18,13 +18,14 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "activerecord", ">
|
21
|
+
spec.add_dependency "activerecord", "> 5.0", "< 7"
|
22
22
|
spec.add_dependency "activesupport"
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler"
|
25
|
-
spec.add_development_dependency "rake", "~>
|
25
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
26
26
|
spec.add_development_dependency "rspec"
|
27
27
|
spec.add_development_dependency "database_cleaner"
|
28
28
|
spec.add_development_dependency "sqlite3"
|
29
29
|
spec.add_development_dependency "byebug"
|
30
|
+
spec.add_development_dependency "db-query-matchers"
|
30
31
|
end
|
data/lib/jit_preloader.rb
CHANGED
@@ -8,16 +8,17 @@ 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
|
-
if Gem::Version.new(ActiveRecord::VERSION::STRING)
|
11
|
+
if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
|
12
|
+
require 'jit_preloader/active_record/associations/preloader/ar6_association'
|
13
|
+
elsif Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("5.2.2")
|
14
|
+
require 'jit_preloader/active_record/associations/preloader/ar5_association'
|
15
|
+
else
|
12
16
|
require 'jit_preloader/active_record/associations/preloader/collection_association'
|
13
17
|
require 'jit_preloader/active_record/associations/preloader/singular_association'
|
14
|
-
else
|
15
|
-
require 'jit_preloader/active_record/associations/preloader/association'
|
16
18
|
end
|
17
19
|
require 'jit_preloader/preloader'
|
18
20
|
|
19
21
|
module JitPreloader
|
20
|
-
|
21
22
|
def self.globally_enabled=(value)
|
22
23
|
@enabled = value
|
23
24
|
end
|
data/lib/jit_preloader/active_record/associations/preloader/{association.rb → ar5_association.rb}
RENAMED
@@ -18,20 +18,11 @@ module JitPreloader
|
|
18
18
|
# end
|
19
19
|
|
20
20
|
def run(preloader)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
association.set_inverse_instance(record)
|
21
|
+
super.tap do
|
22
|
+
if preloaded_records.any? && preloaded_records.none?(&:jit_preloader)
|
23
|
+
JitPreloader::Preloader.attach(preloaded_records) if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
|
24
|
+
end
|
26
25
|
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
26
|
end
|
36
27
|
|
37
28
|
# Original method:
|
@@ -54,6 +45,7 @@ module JitPreloader
|
|
54
45
|
# the original copy so that we don't blow away in-memory changes.
|
55
46
|
new_records = association.target.any? ? records - association.target : records
|
56
47
|
association.target.concat(new_records)
|
48
|
+
association.loaded!
|
57
49
|
else
|
58
50
|
association.target ||= records.first unless records.empty?
|
59
51
|
end
|
@@ -69,3 +61,4 @@ module JitPreloader
|
|
69
61
|
end
|
70
62
|
|
71
63
|
ActiveRecord::Associations::Preloader::Association.prepend(JitPreloader::PreloaderAssociation)
|
64
|
+
ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(JitPreloader::PreloaderAssociation)
|
@@ -0,0 +1,55 @@
|
|
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
|
9
|
+
# records = records_by_owner
|
10
|
+
|
11
|
+
# owners.each do |owner|
|
12
|
+
# associate_records_to_owner(owner, records[owner] || [])
|
13
|
+
# end if @associate
|
14
|
+
|
15
|
+
# self
|
16
|
+
# end
|
17
|
+
|
18
|
+
def run
|
19
|
+
super.tap do
|
20
|
+
if preloaded_records.any? && preloaded_records.none?(&:jit_preloader)
|
21
|
+
JitPreloader::Preloader.attach(preloaded_records) if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Original method:
|
27
|
+
# def associate_records_to_owner(owner, records)
|
28
|
+
# association = owner.association(reflection.name)
|
29
|
+
# if reflection.collection?
|
30
|
+
# association.target = records
|
31
|
+
# else
|
32
|
+
# association.target = records.first
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
def associate_records_to_owner(owner, records)
|
36
|
+
association = owner.association(reflection.name)
|
37
|
+
if reflection.collection?
|
38
|
+
new_records = association.target.any? ? records - association.target : records
|
39
|
+
association.target.concat(new_records)
|
40
|
+
association.loaded!
|
41
|
+
else
|
42
|
+
association.target = records.first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_scope
|
47
|
+
super.tap do |scope|
|
48
|
+
scope.jit_preload! if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
ActiveRecord::Associations::Preloader::Association.prepend(JitPreloader::PreloaderAssociation)
|
55
|
+
ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(JitPreloader::PreloaderAssociation)
|
@@ -2,6 +2,7 @@ module JitPreloadExtension
|
|
2
2
|
attr_accessor :jit_preloader
|
3
3
|
attr_accessor :jit_n_plus_one_tracking
|
4
4
|
attr_accessor :jit_preload_aggregates
|
5
|
+
attr_accessor :jit_preload_scoped_relations
|
5
6
|
|
6
7
|
def reload(*args)
|
7
8
|
clear_jit_preloader!
|
@@ -10,17 +11,73 @@ module JitPreloadExtension
|
|
10
11
|
|
11
12
|
def clear_jit_preloader!
|
12
13
|
self.jit_preload_aggregates = {}
|
14
|
+
self.jit_preload_scoped_relations = {}
|
13
15
|
if jit_preloader
|
14
16
|
jit_preloader.records.delete(self)
|
15
17
|
self.jit_preloader = nil
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
21
|
+
if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
|
22
|
+
def preload_scoped_relation(name:, base_association:, preload_scope: nil)
|
23
|
+
return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
|
24
|
+
|
25
|
+
records = jit_preloader&.records || [self]
|
26
|
+
|
27
|
+
preloader_association = ActiveRecord::Associations::Preloader.new.preload(
|
28
|
+
records,
|
29
|
+
base_association,
|
30
|
+
preload_scope
|
31
|
+
).first
|
32
|
+
|
33
|
+
records.each do |record|
|
34
|
+
record.jit_preload_scoped_relations ||= {}
|
35
|
+
association = record.association(base_association)
|
36
|
+
record.jit_preload_scoped_relations[name] = preloader_association.records_by_owner[record]
|
37
|
+
end
|
38
|
+
|
39
|
+
jit_preload_scoped_relations[name]
|
40
|
+
end
|
41
|
+
else
|
42
|
+
def preload_scoped_relation(name:, base_association:, preload_scope: nil)
|
43
|
+
return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
|
44
|
+
|
45
|
+
records = jit_preloader&.records || [self]
|
46
|
+
previous_association_values = {}
|
47
|
+
|
48
|
+
records.each do |record|
|
49
|
+
association = record.association(base_association)
|
50
|
+
if association.loaded?
|
51
|
+
previous_association_values[record] = association.target
|
52
|
+
association.reset
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
ActiveRecord::Associations::Preloader.new.preload(
|
57
|
+
records,
|
58
|
+
base_association,
|
59
|
+
preload_scope
|
60
|
+
)
|
61
|
+
|
62
|
+
records.each do |record|
|
63
|
+
record.jit_preload_scoped_relations ||= {}
|
64
|
+
association = record.association(base_association)
|
65
|
+
record.jit_preload_scoped_relations[name] = association.target
|
66
|
+
association.reset
|
67
|
+
if previous_association_values.key?(record)
|
68
|
+
association.target = previous_association_values[record]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
jit_preload_scoped_relations[name]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
19
76
|
def self.prepended(base)
|
20
77
|
class << base
|
21
78
|
delegate :jit_preload, to: :all
|
22
79
|
|
23
|
-
def has_many_aggregate(assoc, name, aggregate, field, default: 0)
|
80
|
+
def has_many_aggregate(assoc, name, aggregate, field, table_alias_name: nil, default: 0)
|
24
81
|
method_name = "#{assoc}_#{name}"
|
25
82
|
|
26
83
|
define_method(method_name) do |conditions={}|
|
@@ -41,11 +98,25 @@ module JitPreloadExtension
|
|
41
98
|
association_scope = klass.all.merge(association(assoc).scope).unscope(where: aggregate_association.foreign_key)
|
42
99
|
association_scope = association_scope.instance_exec(&reflection.scope).reorder(nil) if reflection.scope
|
43
100
|
|
44
|
-
|
101
|
+
# If the query uses an alias for the association, use that instead of the table name
|
102
|
+
table_reference = table_alias_name
|
103
|
+
table_reference ||= association_scope.references_values.first || aggregate_association.table_name
|
104
|
+
|
105
|
+
conditions[table_reference] = { aggregate_association.foreign_key => primary_ids }
|
106
|
+
|
107
|
+
# If the association is a STI child model, specify its type in the condition so that it
|
108
|
+
# doesn't include results from other child models
|
109
|
+
parent_is_base_class = aggregate_association.klass.superclass.abstract_class? || aggregate_association.klass.superclass == ActiveRecord::Base
|
110
|
+
has_type_column = aggregate_association.klass.column_names.include?(aggregate_association.klass.inheritance_column)
|
111
|
+
is_child_sti_model = !parent_is_base_class && has_type_column
|
112
|
+
if is_child_sti_model
|
113
|
+
conditions[table_reference].merge!({ aggregate_association.klass.inheritance_column => aggregate_association.klass.sti_name })
|
114
|
+
end
|
115
|
+
|
45
116
|
if reflection.type.present?
|
46
117
|
conditions[reflection.type] = self.class.name
|
47
118
|
end
|
48
|
-
group_by = "#{
|
119
|
+
group_by = "#{table_reference}.#{aggregate_association.foreign_key}"
|
49
120
|
|
50
121
|
preloaded_data = Hash[association_scope
|
51
122
|
.where(conditions)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "db-query-matchers"
|
3
|
+
|
4
|
+
RSpec.describe "ActiveRecord::Base Extensions" do
|
5
|
+
|
6
|
+
let(:canada) { Country.create(name: "Canada") }
|
7
|
+
let(:usa) { Country.create(name: "U.S.A") }
|
8
|
+
|
9
|
+
describe "#preload_scoped_relation" do
|
10
|
+
def call(contact)
|
11
|
+
contact.preload_scoped_relation(
|
12
|
+
name: "American Addresses",
|
13
|
+
base_association: :addresses,
|
14
|
+
preload_scope: Address.where(country: usa)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
Contact.create(name: "Bar", addresses: [
|
20
|
+
Address.new(street: "123 Fake st", country: canada),
|
21
|
+
Address.new(street: "21 Jump st", country: usa),
|
22
|
+
Address.new(street: "90210 Beverly Hills", country: usa)
|
23
|
+
])
|
24
|
+
|
25
|
+
Contact.create(name: "Foo", addresses: [
|
26
|
+
Address.new(street: "1 First st", country: canada),
|
27
|
+
Address.new(street: "10 Tenth Ave", country: usa)
|
28
|
+
])
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when operating on a single object" do
|
32
|
+
it "will load the objects for that object" do
|
33
|
+
contact = Contact.first
|
34
|
+
expect(call(contact)).to match_array contact.addresses.where(country: usa).to_a
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "memoizes the result" do
|
39
|
+
contacts = Contact.jit_preload.limit(2).to_a
|
40
|
+
|
41
|
+
expect do
|
42
|
+
expect(call(contacts.first))
|
43
|
+
expect(call(contacts.first))
|
44
|
+
end.to make_database_queries(count: 1)
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when reloading the object" do
|
48
|
+
it "clears the memoization" do
|
49
|
+
contacts = Contact.jit_preload.limit(2).to_a
|
50
|
+
|
51
|
+
expect do
|
52
|
+
expect(call(contacts.first))
|
53
|
+
end.to make_database_queries(count: 1)
|
54
|
+
contacts.first.reload
|
55
|
+
expect do
|
56
|
+
expect(call(contacts.first))
|
57
|
+
end.to make_database_queries(count: 1)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "will issue one query for the group of objects" do
|
62
|
+
contacts = Contact.jit_preload.limit(2).to_a
|
63
|
+
|
64
|
+
usa_addresses = contacts.first.addresses.where(country: usa).to_a
|
65
|
+
expect do
|
66
|
+
expect(call(contacts.first)).to match_array usa_addresses
|
67
|
+
end.to make_database_queries(count: 1)
|
68
|
+
|
69
|
+
usa_addresses = contacts.last.addresses.where(country: usa).to_a
|
70
|
+
expect do
|
71
|
+
expect(call(contacts.last)).to match_array usa_addresses
|
72
|
+
end.to_not make_database_queries
|
73
|
+
end
|
74
|
+
|
75
|
+
it "doesn't load the value into the association" do
|
76
|
+
contacts = Contact.jit_preload.limit(2).to_a
|
77
|
+
call(contacts.first)
|
78
|
+
|
79
|
+
expect(contacts.first.association(:addresses)).to_not be_loaded
|
80
|
+
expect(contacts.last.association(:addresses)).to_not be_loaded
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when the association is already loaded" do
|
84
|
+
it "doesn't change the value of the association" do
|
85
|
+
contacts = Contact.jit_preload.limit(2).to_a
|
86
|
+
contacts.each{|contact| contact.addresses.to_a }
|
87
|
+
contacts.each{|contact| call(contact) }
|
88
|
+
|
89
|
+
expect(contacts.first.association(:addresses)).to be_loaded
|
90
|
+
expect(contacts.last.association(:addresses)).to be_loaded
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe JitPreloader::Preloader do
|
4
|
-
|
5
4
|
let!(:contact1) do
|
6
5
|
addresses = [
|
7
6
|
Address.new(street: "123 Fake st", country: canada),
|
@@ -49,6 +48,79 @@ RSpec.describe JitPreloader::Preloader do
|
|
49
48
|
->(event, data){ source_map[data[:source]] << data[:association] }
|
50
49
|
end
|
51
50
|
|
51
|
+
context "for single table inheritance" do
|
52
|
+
context "when preloading an aggregate for a child model" do
|
53
|
+
let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
|
54
|
+
let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) }
|
55
|
+
let!(:company2) { Company.create(name: "Company2", contact_book: contact_book) }
|
56
|
+
|
57
|
+
it "can handle queries" do
|
58
|
+
contact_books = ContactBook.jit_preload.to_a
|
59
|
+
expect(contact_books.first.companies_count).to eq 2
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when preloading an aggregate of a child model through its base model" do
|
64
|
+
let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
|
65
|
+
let!(:contact) { Contact.create(name: "Contact", contact_book: contact_book) }
|
66
|
+
let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) }
|
67
|
+
let!(:company2) { Company.create(name: "Company2", contact_book: contact_book) }
|
68
|
+
let!(:contact_employee1) { Employee.create(name: "Contact Employee1", contact: contact) }
|
69
|
+
let!(:contact_employee2) { Employee.create(name: "Contact Employee2", contact: contact) }
|
70
|
+
let!(:company_employee1) { Employee.create(name: "Company Employee1", contact: company1) }
|
71
|
+
let!(:company_employee2) { Employee.create(name: "Company Employee2", contact: company2) }
|
72
|
+
|
73
|
+
it "can handle queries" do
|
74
|
+
contact_books = ContactBook.jit_preload.to_a
|
75
|
+
expect(contact_books.first.employees_count).to eq 4
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "when preloading an aggregate of a nested child model through another child model" do
|
80
|
+
let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
|
81
|
+
let!(:contact) { Contact.create(name: "Contact", contact_book: contact_book) }
|
82
|
+
let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) }
|
83
|
+
let!(:company2) { Company.create(name: "Company2", contact_book: contact_book) }
|
84
|
+
let!(:contact_employee1) { Employee.create(name: "Contact Employee1", contact: contact) }
|
85
|
+
let!(:contact_employee2) { Employee.create(name: "Contact Employee2", contact: contact) }
|
86
|
+
let!(:company_employee1) { Employee.create(name: "Company Employee1", contact: company1) }
|
87
|
+
let!(:company_employee2) { Employee.create(name: "Company Employee2", contact: company2) }
|
88
|
+
|
89
|
+
it "can handle queries" do
|
90
|
+
contact_books = ContactBook.jit_preload.to_a
|
91
|
+
expect(contact_books.first.company_employees_count).to eq 2
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when preloading an aggregate of a nested child model through a many-to-many relationship with another child model" do
|
96
|
+
let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
|
97
|
+
let!(:child1) { Child.create(name: "Child1") }
|
98
|
+
let!(:child2) { Child.create(name: "Child2") }
|
99
|
+
let!(:child3) { Child.create(name: "Child3") }
|
100
|
+
let!(:parent1) { Parent.create(name: "Parent1", contact_book: contact_book, children: [child1, child2]) }
|
101
|
+
let!(:parent2) { Parent.create(name: "Parent2", contact_book: contact_book, children: [child2, child3]) }
|
102
|
+
|
103
|
+
it "can handle queries" do
|
104
|
+
contact_books = ContactBook.jit_preload.to_a
|
105
|
+
expect(contact_books.first.children_count).to eq 4
|
106
|
+
expect(contact_books.first.children).to include(child1, child2, child3)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "when preloading an aggregate for a child model scoped by another join table" do
|
111
|
+
let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
|
112
|
+
let!(:contact1) { Company.create(name: "Without Email", contact_book: contact_book) }
|
113
|
+
let!(:contact2) { Company.create(name: "With Blank Email", email_address: EmailAddress.new(address: ""), contact_book: contact_book) }
|
114
|
+
let!(:contact3) { Company.create(name: "With Email", email_address: EmailAddress.new(address: "a@a.com"), contact_book: contact_book) }
|
115
|
+
|
116
|
+
it "can handle queries" do
|
117
|
+
contact_books = ContactBook.jit_preload.to_a
|
118
|
+
expect(contact_books.first.companies_with_blank_email_address_count).to eq 1
|
119
|
+
expect(contact_books.first.companies_with_blank_email_address).to eq [contact2]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
52
124
|
context "when preloading an aggregate as polymorphic" do
|
53
125
|
let(:contact_owner_counts) { [2] }
|
54
126
|
|
data/spec/support/database.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
class Database
|
2
2
|
def self.tables
|
3
3
|
[
|
4
|
-
"CREATE TABLE
|
4
|
+
"CREATE TABLE contact_books (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
5
|
+
"CREATE TABLE contacts (id INTEGER NOT NULL PRIMARY KEY, type VARCHAR(255), contact_book_id INTEGER, contact_id INTEGER, name VARCHAR(255), contact_owner_id INTEGER, contact_owner_type VARCHAR(255))",
|
5
6
|
"CREATE TABLE contact_owners (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
6
7
|
"CREATE TABLE addresses (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, country_id INTEGER NOT NULL, street VARCHAR(255))",
|
7
8
|
"CREATE TABLE email_addresses (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, address VARCHAR(255))",
|
8
9
|
"CREATE TABLE phone_numbers (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, phone VARCHAR(10))",
|
9
10
|
"CREATE TABLE countries (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
11
|
+
"CREATE TABLE parents_children (id INTEGER NOT NULL PRIMARY KEY, parent_id INTEGER, child_id INTEGER)",
|
10
12
|
]
|
11
13
|
end
|
12
14
|
|
data/spec/support/models.rb
CHANGED
@@ -1,15 +1,58 @@
|
|
1
|
+
class ContactBook < ActiveRecord::Base
|
2
|
+
has_many :contacts
|
3
|
+
has_many :employees, through: :contacts
|
4
|
+
|
5
|
+
has_many :companies
|
6
|
+
has_many :company_employees, through: :companies, source: :employees
|
7
|
+
|
8
|
+
has_many :parents
|
9
|
+
has_many :children, through: :parents
|
10
|
+
|
11
|
+
has_many_aggregate :companies, :count, :count, "*"
|
12
|
+
has_many_aggregate :employees, :count, :count, "*"
|
13
|
+
has_many_aggregate :company_employees, :count, :count, "*"
|
14
|
+
has_many_aggregate :children, :count, :count, "*"
|
15
|
+
|
16
|
+
has_many :companies_with_blank_email_address, -> { joins(:email_address).where(email_addresses: { address: "" }) }, class_name: "Company"
|
17
|
+
has_many_aggregate :companies_with_blank_email_address, :count, :count, "*", table_alias_name: "contacts"
|
18
|
+
end
|
19
|
+
|
1
20
|
class Contact < ActiveRecord::Base
|
21
|
+
belongs_to :contact_book
|
2
22
|
belongs_to :contact_owner, polymorphic: true
|
3
23
|
|
4
24
|
has_many :addresses
|
5
25
|
has_many :phone_numbers
|
6
26
|
has_one :email_address
|
27
|
+
has_many :employees
|
7
28
|
|
8
29
|
has_many_aggregate :addresses, :max_street_length, :maximum, "LENGTH(street)"
|
9
30
|
has_many_aggregate :phone_numbers, :count, :count, "id"
|
10
31
|
has_many_aggregate :addresses, :count, :count, "*"
|
11
32
|
end
|
12
33
|
|
34
|
+
class Company < Contact
|
35
|
+
end
|
36
|
+
|
37
|
+
class Employee < Contact
|
38
|
+
belongs_to :contact
|
39
|
+
end
|
40
|
+
|
41
|
+
class ParentsChild < ActiveRecord::Base
|
42
|
+
belongs_to :parent
|
43
|
+
belongs_to :child
|
44
|
+
end
|
45
|
+
|
46
|
+
class Parent < Contact
|
47
|
+
has_many :parents_child
|
48
|
+
has_many :children, through: :parents_child
|
49
|
+
end
|
50
|
+
|
51
|
+
class Child < Contact
|
52
|
+
has_many :parents_child
|
53
|
+
has_many :parents, through: :parents_child
|
54
|
+
end
|
55
|
+
|
13
56
|
class Address < ActiveRecord::Base
|
14
57
|
belongs_to :contact
|
15
58
|
belongs_to :country
|
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: 1.0.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: 2020-11-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,20 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.0'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '7'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">"
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
29
|
+
version: '5.0'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '7'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: activesupport
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,14 +64,14 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: '
|
67
|
+
version: '13.0'
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: '
|
74
|
+
version: '13.0'
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: rspec
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -128,6 +128,20 @@ dependencies:
|
|
128
128
|
- - ">="
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: db-query-matchers
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
131
145
|
description: The JitPreloader has the ability to send notifications when N+1 queries
|
132
146
|
occur to help guage how problematic they are for your code base and a way to remove
|
133
147
|
all of the commons explicitly or automatically
|
@@ -140,14 +154,14 @@ files:
|
|
140
154
|
- ".gitignore"
|
141
155
|
- ".rspec"
|
142
156
|
- Gemfile
|
143
|
-
- Gemfile.lock
|
144
157
|
- LICENSE
|
145
158
|
- README.md
|
146
159
|
- Rakefile
|
147
160
|
- jit_preloader.gemspec
|
148
161
|
- lib/jit_preloader.rb
|
149
162
|
- lib/jit_preloader/active_record/associations/collection_association.rb
|
150
|
-
- lib/jit_preloader/active_record/associations/preloader/
|
163
|
+
- lib/jit_preloader/active_record/associations/preloader/ar5_association.rb
|
164
|
+
- lib/jit_preloader/active_record/associations/preloader/ar6_association.rb
|
151
165
|
- lib/jit_preloader/active_record/associations/preloader/collection_association.rb
|
152
166
|
- lib/jit_preloader/active_record/associations/preloader/singular_association.rb
|
153
167
|
- lib/jit_preloader/active_record/associations/singular_association.rb
|
@@ -155,6 +169,7 @@ files:
|
|
155
169
|
- lib/jit_preloader/active_record/relation.rb
|
156
170
|
- lib/jit_preloader/preloader.rb
|
157
171
|
- lib/jit_preloader/version.rb
|
172
|
+
- spec/lib/jit_preloader/active_record/base_spec.rb
|
158
173
|
- spec/lib/jit_preloader/preloader_spec.rb
|
159
174
|
- spec/spec_helper.rb
|
160
175
|
- spec/support/database.rb
|
@@ -178,11 +193,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
193
|
- !ruby/object:Gem::Version
|
179
194
|
version: '0'
|
180
195
|
requirements: []
|
181
|
-
rubygems_version: 3.
|
196
|
+
rubygems_version: 3.1.4
|
182
197
|
signing_key:
|
183
198
|
specification_version: 4
|
184
199
|
summary: Tool to understand N+1 queries and to remove them
|
185
200
|
test_files:
|
201
|
+
- spec/lib/jit_preloader/active_record/base_spec.rb
|
186
202
|
- spec/lib/jit_preloader/preloader_spec.rb
|
187
203
|
- spec/spec_helper.rb
|
188
204
|
- spec/support/database.rb
|
data/Gemfile.lock
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
jit_preloader (0.2.1)
|
5
|
-
activerecord (> 4.2, < 6)
|
6
|
-
activesupport
|
7
|
-
|
8
|
-
GEM
|
9
|
-
remote: https://rubygems.org/
|
10
|
-
specs:
|
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
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
19
|
-
i18n (>= 0.7, < 2)
|
20
|
-
minitest (~> 5.1)
|
21
|
-
tzinfo (~> 1.1)
|
22
|
-
arel (9.0.0)
|
23
|
-
byebug (9.0.6)
|
24
|
-
concurrent-ruby (1.1.4)
|
25
|
-
database_cleaner (1.5.3)
|
26
|
-
diff-lcs (1.2.5)
|
27
|
-
i18n (1.6.0)
|
28
|
-
concurrent-ruby (~> 1.0)
|
29
|
-
minitest (5.11.3)
|
30
|
-
rake (10.5.0)
|
31
|
-
rspec (3.5.0)
|
32
|
-
rspec-core (~> 3.5.0)
|
33
|
-
rspec-expectations (~> 3.5.0)
|
34
|
-
rspec-mocks (~> 3.5.0)
|
35
|
-
rspec-core (3.5.4)
|
36
|
-
rspec-support (~> 3.5.0)
|
37
|
-
rspec-expectations (3.5.0)
|
38
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
39
|
-
rspec-support (~> 3.5.0)
|
40
|
-
rspec-mocks (3.5.0)
|
41
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
-
rspec-support (~> 3.5.0)
|
43
|
-
rspec-support (3.5.0)
|
44
|
-
sqlite3 (1.3.12)
|
45
|
-
thread_safe (0.3.6)
|
46
|
-
tzinfo (1.2.5)
|
47
|
-
thread_safe (~> 0.1)
|
48
|
-
|
49
|
-
PLATFORMS
|
50
|
-
ruby
|
51
|
-
|
52
|
-
DEPENDENCIES
|
53
|
-
bundler
|
54
|
-
byebug
|
55
|
-
database_cleaner
|
56
|
-
jit_preloader!
|
57
|
-
rake (~> 10.0)
|
58
|
-
rspec
|
59
|
-
sqlite3
|
60
|
-
|
61
|
-
BUNDLED WITH
|
62
|
-
2.0.1
|