jit_preloader 0.2.3 → 0.2.4
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 +5 -5
- data/Gemfile.lock +17 -13
- data/README.md +37 -1
- data/jit_preloader.gemspec +2 -1
- data/lib/jit_preloader/active_record/base.rb +35 -0
- data/lib/jit_preloader/version.rb +1 -1
- data/spec/lib/jit_preloader/active_record/base_spec.rb +94 -0
- metadata +21 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c697833105982c2f6ee8312618f5d32550c493c00c2b8ca76c6ca1895e387171
|
4
|
+
data.tar.gz: c679aad87494ee39f689ef1e9d071897b78308c3b0c25847009703ec0712e741
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6eb1c49ec80fb2b9abf6cba22a38c55043de01e6d9138026b92ce000c460dd2dda20dc72d18ba94cd18b9393dea2cbc2fccfc4806ea6aa93b97bb323a697be9a
|
7
|
+
data.tar.gz: 82b70f1ac6a12c6c87a980f06c1e85802fc5b16f566651c1945ed663d698b11ede6b0b0a4dbc2010b943d18d6955612c78f3281080ef8817e7e7fdc440979ec7
|
data/Gemfile.lock
CHANGED
@@ -1,33 +1,36 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
jit_preloader (0.2.
|
4
|
+
jit_preloader (0.2.3)
|
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.2.2)
|
12
|
-
activesupport (= 5.2.2)
|
13
|
-
activerecord (5.2.2)
|
14
|
-
activemodel (= 5.2.2)
|
15
|
-
activesupport (= 5.2.2)
|
11
|
+
activemodel (5.2.4.2)
|
12
|
+
activesupport (= 5.2.4.2)
|
13
|
+
activerecord (5.2.4.2)
|
14
|
+
activemodel (= 5.2.4.2)
|
15
|
+
activesupport (= 5.2.4.2)
|
16
16
|
arel (>= 9.0)
|
17
|
-
activesupport (5.2.2)
|
17
|
+
activesupport (5.2.4.2)
|
18
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
19
19
|
i18n (>= 0.7, < 2)
|
20
20
|
minitest (~> 5.1)
|
21
21
|
tzinfo (~> 1.1)
|
22
22
|
arel (9.0.0)
|
23
23
|
byebug (9.0.6)
|
24
|
-
concurrent-ruby (1.1.
|
24
|
+
concurrent-ruby (1.1.6)
|
25
25
|
database_cleaner (1.5.3)
|
26
|
+
db-query-matchers (0.10.0)
|
27
|
+
activesupport (>= 4.0, < 7)
|
28
|
+
rspec (~> 3.0)
|
26
29
|
diff-lcs (1.2.5)
|
27
|
-
i18n (1.
|
30
|
+
i18n (1.8.2)
|
28
31
|
concurrent-ruby (~> 1.0)
|
29
|
-
minitest (5.
|
30
|
-
rake (
|
32
|
+
minitest (5.14.0)
|
33
|
+
rake (13.0.1)
|
31
34
|
rspec (3.5.0)
|
32
35
|
rspec-core (~> 3.5.0)
|
33
36
|
rspec-expectations (~> 3.5.0)
|
@@ -43,7 +46,7 @@ GEM
|
|
43
46
|
rspec-support (3.5.0)
|
44
47
|
sqlite3 (1.3.12)
|
45
48
|
thread_safe (0.3.6)
|
46
|
-
tzinfo (1.2.
|
49
|
+
tzinfo (1.2.6)
|
47
50
|
thread_safe (~> 0.1)
|
48
51
|
|
49
52
|
PLATFORMS
|
@@ -53,8 +56,9 @@ DEPENDENCIES
|
|
53
56
|
bundler
|
54
57
|
byebug
|
55
58
|
database_cleaner
|
59
|
+
db-query-matchers
|
56
60
|
jit_preloader!
|
57
|
-
rake (~>
|
61
|
+
rake (~> 13.0)
|
58
62
|
rspec
|
59
63
|
sqlite3
|
60
64
|
|
data/README.md
CHANGED
@@ -173,6 +173,42 @@ 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
|
+
|
176
212
|
### Jit preloading globally across your application
|
177
213
|
|
178
214
|
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 +237,7 @@ This is mostly a magic bullet, but it doesn't solve all database-related problem
|
|
201
237
|
```ruby
|
202
238
|
Contact.all.each do |contact|
|
203
239
|
contact.emails.reload # Reloading the association
|
204
|
-
contact.addresses.where(billing: true).to_a # Querying the association
|
240
|
+
contact.addresses.where(billing: true).to_a # Querying the association (Use: preload_scoped_relation to avoid these)
|
205
241
|
end
|
206
242
|
```
|
207
243
|
|
data/jit_preloader.gemspec
CHANGED
@@ -22,9 +22,10 @@ Gem::Specification.new do |spec|
|
|
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
|
@@ -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,12 +11,46 @@ 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
|
+
def preload_scoped_relation(name:, base_association:, preload_scope: nil)
|
22
|
+
return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
|
23
|
+
|
24
|
+
records = jit_preloader&.records || [self]
|
25
|
+
previous_association_values = {}
|
26
|
+
|
27
|
+
records.each do |record|
|
28
|
+
association = record.association(base_association)
|
29
|
+
if association.loaded?
|
30
|
+
previous_association_values[record] = association.target
|
31
|
+
association.reset
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
ActiveRecord::Associations::Preloader.new.preload(
|
36
|
+
records,
|
37
|
+
base_association,
|
38
|
+
preload_scope
|
39
|
+
)
|
40
|
+
|
41
|
+
records.each do |record|
|
42
|
+
record.jit_preload_scoped_relations ||= {}
|
43
|
+
association = record.association(base_association)
|
44
|
+
record.jit_preload_scoped_relations[name] = association.target
|
45
|
+
association.reset
|
46
|
+
if previous_association_values.key?(record)
|
47
|
+
association.target = previous_association_values[record]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
jit_preload_scoped_relations[name]
|
52
|
+
end
|
53
|
+
|
19
54
|
def self.prepended(base)
|
20
55
|
class << base
|
21
56
|
delegate :jit_preload, to: :all
|
@@ -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
|
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.2.
|
4
|
+
version: 0.2.4
|
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-04-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -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
|
@@ -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,12 +193,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
193
|
- !ruby/object:Gem::Version
|
179
194
|
version: '0'
|
180
195
|
requirements: []
|
181
|
-
|
182
|
-
rubygems_version: 2.5.2.1
|
196
|
+
rubygems_version: 3.0.6
|
183
197
|
signing_key:
|
184
198
|
specification_version: 4
|
185
199
|
summary: Tool to understand N+1 queries and to remove them
|
186
200
|
test_files:
|
201
|
+
- spec/lib/jit_preloader/active_record/base_spec.rb
|
187
202
|
- spec/lib/jit_preloader/preloader_spec.rb
|
188
203
|
- spec/spec_helper.rb
|
189
204
|
- spec/support/database.rb
|