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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c2c802d6683cb058781abe0360bec58b0857c020
4
- data.tar.gz: 3576db9546592974cc8383c26fa395b939971876
2
+ SHA256:
3
+ metadata.gz: c697833105982c2f6ee8312618f5d32550c493c00c2b8ca76c6ca1895e387171
4
+ data.tar.gz: c679aad87494ee39f689ef1e9d071897b78308c3b0c25847009703ec0712e741
5
5
  SHA512:
6
- metadata.gz: 4cf99a2440af7e4ef861cf2a2ea679f6ff3fac90494aab0fddb5722a1712b9611357d03c15d6cf1a1ced43f698231228c276c517b362fb05937f8e3510a7a8e4
7
- data.tar.gz: ba29dd13180a27824ce4e1ad1ec88124c1200e6025a5341a88aff891b706cb1f899a6ba6d59c208c2ae042cc8112df0ea9c4fed90b3e2a7b6d428145b39b5f1b
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.1)
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.4)
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.6.0)
30
+ i18n (1.8.2)
28
31
  concurrent-ruby (~> 1.0)
29
- minitest (5.11.3)
30
- rake (10.5.0)
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.5)
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 (~> 10.0)
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
 
@@ -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", "~> 10.0"
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
@@ -1,3 +1,3 @@
1
1
  module JitPreloader
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
@@ -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.3
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: 2019-11-15 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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
- rubyforge_project:
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