jit_preloader 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +61 -0
- data/LICENSE +21 -0
- data/README.md +187 -0
- data/Rakefile +2 -0
- data/jit_preloader.gemspec +30 -0
- data/lib/jit_preloader/active_record/associations/collection_association.rb +30 -0
- data/lib/jit_preloader/active_record/associations/preloader/collection_association.rb +34 -0
- data/lib/jit_preloader/active_record/associations/preloader/singular_association.rb +29 -0
- data/lib/jit_preloader/active_record/associations/singular_association.rb +29 -0
- data/lib/jit_preloader/active_record/base.rb +15 -0
- data/lib/jit_preloader/active_record/query_methods.rb +16 -0
- data/lib/jit_preloader/active_record/relation.rb +26 -0
- data/lib/jit_preloader/preloader.rb +23 -0
- data/lib/jit_preloader/version.rb +3 -0
- data/lib/jit_preloader.rb +30 -0
- data/spec/lib/jit_preloader/preloader_spec.rb +144 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/support/database.rb +21 -0
- data/spec/support/models.rb +17 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f7e698f4bfc6d6724f1b11f46b954995085525b7
|
4
|
+
data.tar.gz: 5e597f8c4289ac459d030d4f1c42f76bbf327d34
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2469c5a0361154980988d9c5bf0aae13842b583051d168656efadf9a21f7fc8379d68c842831cce6e1b4b4d37253fb294ddfe84618f3786f35262459590bf9df
|
7
|
+
data.tar.gz: 8127d7c2040f18b21664fc11a5062db501d87e378fdbf91b4d327f86819d2a8df36f6c760c3fab0e6dcf79dcc340c71d5f05db62fe01287a20cd6cf720d53fc3
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
jit_preloader (0.0.1)
|
5
|
+
activerecord (~> 4.2)
|
6
|
+
activesupport
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (4.2.7.1)
|
12
|
+
activesupport (= 4.2.7.1)
|
13
|
+
builder (~> 3.1)
|
14
|
+
activerecord (4.2.7.1)
|
15
|
+
activemodel (= 4.2.7.1)
|
16
|
+
activesupport (= 4.2.7.1)
|
17
|
+
arel (~> 6.0)
|
18
|
+
activesupport (4.2.7.1)
|
19
|
+
i18n (~> 0.7)
|
20
|
+
json (~> 1.7, >= 1.7.7)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
23
|
+
tzinfo (~> 1.1)
|
24
|
+
arel (6.0.3)
|
25
|
+
builder (3.2.2)
|
26
|
+
byebug (9.0.6)
|
27
|
+
database_cleaner (1.5.3)
|
28
|
+
diff-lcs (1.2.5)
|
29
|
+
i18n (0.7.0)
|
30
|
+
json (1.8.3)
|
31
|
+
minitest (5.9.1)
|
32
|
+
rake (10.5.0)
|
33
|
+
rspec (3.5.0)
|
34
|
+
rspec-core (~> 3.5.0)
|
35
|
+
rspec-expectations (~> 3.5.0)
|
36
|
+
rspec-mocks (~> 3.5.0)
|
37
|
+
rspec-core (3.5.4)
|
38
|
+
rspec-support (~> 3.5.0)
|
39
|
+
rspec-expectations (3.5.0)
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
41
|
+
rspec-support (~> 3.5.0)
|
42
|
+
rspec-mocks (3.5.0)
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
44
|
+
rspec-support (~> 3.5.0)
|
45
|
+
rspec-support (3.5.0)
|
46
|
+
sqlite3 (1.3.12)
|
47
|
+
thread_safe (0.3.5)
|
48
|
+
tzinfo (1.2.2)
|
49
|
+
thread_safe (~> 0.1)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
bundler (~> 1.7)
|
56
|
+
byebug
|
57
|
+
database_cleaner
|
58
|
+
jit_preloader!
|
59
|
+
rake (~> 10.0)
|
60
|
+
rspec
|
61
|
+
sqlite3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Kyle d'Oliveira
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# JitPreloader
|
2
|
+
|
3
|
+
N+1 queries are a silent killer for performance. Sometimes they can be noticeable; other times they're just a minor tax. We want a way to remove them.
|
4
|
+
|
5
|
+
Imagine you have contacts that have many emails, phone numbers, and addresses. You might have code like this:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
def do_my_thing(contact)
|
9
|
+
contact.emails.each do |email|
|
10
|
+
# Do a thing with the email
|
11
|
+
end
|
12
|
+
contact.phone_numbers.each do |phone_number|
|
13
|
+
# Do a thing with the phone number
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# This will generate two N+1 queries, one for emails and one for phone numbers.
|
18
|
+
Contact.all.each do |contact|
|
19
|
+
do_my_thing(contact)
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
Rails solves this with `includes` (or better, `preload`/`eager_load`, as they are what `includes` uses in the background). So to get around this problem in Rails you would do something like this:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
Contact.preload(:emails, :phone_numbers).each do |contact|
|
27
|
+
do_my_thing(contact)
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
However this does have some limitations.
|
32
|
+
|
33
|
+
1) When doing the `preload`, you have to understand what the code does in order to properly load the associations. When this is a brand new method or a simple method this may be simple, but sometimes it can be difficult or time-consuming to figure this out.
|
34
|
+
|
35
|
+
2) Imagine we change the method to also use the `addresses` association:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
def do_my_thing(contact)
|
39
|
+
contact.emails.each do |email|
|
40
|
+
# Do a thing with the email
|
41
|
+
end
|
42
|
+
contact.phone_numbers.each do |phone_number|
|
43
|
+
# Do a thing with the phone number
|
44
|
+
end
|
45
|
+
contact.addresses.each do |address|
|
46
|
+
# Do a thing with the address
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
All of a sudden we have an N+1 query again. So now you need to go hunt down all of the places were you were preloading and preload the new association.
|
52
|
+
|
53
|
+
3) Imagine we change the method to do this:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
def do_my_thing(contact)
|
57
|
+
contact.emails.each do |email|
|
58
|
+
# Do a thing with the email
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
We don't have an N+1 query here, but now we are preloading the `phone_numbers` association but not doing anything with it. This is still bad, especially when there are a lot of associations on the object.
|
64
|
+
|
65
|
+
This gem provides a "magic bullet" that can remove most N+1 queries in the application.
|
66
|
+
|
67
|
+
## Installation
|
68
|
+
|
69
|
+
Add this line to your application's Gemfile:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
gem 'jit_preloader'
|
73
|
+
```
|
74
|
+
|
75
|
+
And then execute:
|
76
|
+
|
77
|
+
$ bundle
|
78
|
+
|
79
|
+
Or install it yourself as:
|
80
|
+
|
81
|
+
$ gem install jit_preloader
|
82
|
+
|
83
|
+
## Usage
|
84
|
+
|
85
|
+
This gem provides three features:
|
86
|
+
|
87
|
+
### N+1 query tracking
|
88
|
+
|
89
|
+
This gem will publish an `n_plus_one_query` event via ActiveSupport::Notifications whenever it detects one. This lets you do a variety of useful things. Here are some examples:
|
90
|
+
|
91
|
+
You could implement some basic tracking. This will let you measure the extent of the N+1 query problems in your app:
|
92
|
+
```ruby
|
93
|
+
ActiveSupport::Notifications.subscribe("n_plus_one_query") do |event, data|
|
94
|
+
statsd.increment "web.#{Rails.env}.n_plus_one_queries.global"
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
You could log the N+1 queries. In your development environment, you could throw N+1 queries into the logs along with a stack trace:
|
99
|
+
```ruby
|
100
|
+
ActiveSupport::Notifications.subscribe("n_plus_one_query") do |event, data|
|
101
|
+
message = "N+1 Query detected: #{data[:association]} on #{data[:source].class}"
|
102
|
+
backtrace = caller.select{|r| r.starts_with?(Rails.root.to_s) }
|
103
|
+
Rails.logger.debug("\n\n#{message}\n#{backtrace.join("\n")}\n".red)
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
If you use rspec, you could wrap your specs in an `around(:each)` that throws an exception if an N+1 query is detected. You could even provide a tag that allows tests that have known N+1 queries to still pass:
|
108
|
+
```ruby
|
109
|
+
config.around(:each) do |example|
|
110
|
+
callback = ->(event, data) do
|
111
|
+
unless example.metadata[:known_n_plus_one_query]
|
112
|
+
message = "N+1 Query detected: #{data[:source].class} on #{data[:association]}"
|
113
|
+
raise QueryError.new(message)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
117
|
+
example.run
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
### Jit preloading on a case-by-case basis
|
123
|
+
|
124
|
+
There is now a `jit_preload` and `jit_preload!` method on ActiveRecord::Relation objects. This means instead of using `includes`, `preload` or `eager_load` with the association you want to load, you can simply just use `jit_preload`
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# old
|
128
|
+
Contact.preload(:addresses, :email_addresses).each do |contact|
|
129
|
+
contact.addresses.to_a
|
130
|
+
contact.email_addresses.to_a
|
131
|
+
end
|
132
|
+
|
133
|
+
# new
|
134
|
+
Contact.jit_preload.each do |contact|
|
135
|
+
contact.addresses.to_a
|
136
|
+
contact.email_addresses.to_a
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
### Jit preloading globally across your application
|
141
|
+
|
142
|
+
The JitPreloader can be globally enabled, in which case most N+1 queries in your app should just disappear. It is off by default.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
# Can be true or false
|
146
|
+
JitPreloader.globally_enabled = true
|
147
|
+
|
148
|
+
# Can also be given anything that responds to `call`.
|
149
|
+
# You could build a kill switch with Redis (or whatever you'd like)
|
150
|
+
# so that you can turn it on or off dynamically.
|
151
|
+
JitPreloader.globally_enabled = ->{ $redis.get('always_jit_preload') == 'on' }
|
152
|
+
|
153
|
+
# When enabled globally, this would not generate an N+1 query.
|
154
|
+
Contact.all.each do |contact|
|
155
|
+
contact.emails.each do |email|
|
156
|
+
# do something
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
## What it doesn't solve
|
162
|
+
|
163
|
+
This is mostly a magic bullet, but it doesn't solve all database-related problems. If you reload an association, or call a query or aggregate function on the association, it will not remove those extra queries. These problems cannot be solved by using Rails' `preload` so it cannot be solved with the Jit Preloader.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
Contact.all.each do |contact|
|
167
|
+
contact.emails.reload # Reloading the association
|
168
|
+
contact.phone_numbers.max("LENGTH(number)") # Aggregate functions on the association
|
169
|
+
contact.addresses.where(billing: true).to_a # Querying the association
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
## Consequences
|
174
|
+
|
175
|
+
1) This gem introduces more Magic. This is fine, but you should really understand what is going on under the hood. You should understand what makes an N+1 query happen and what this gem is doing to help address it.
|
176
|
+
|
177
|
+
2) We may do more work than you require. If you have turned the preloader on globally but you only want to access a single record's association, it will load the association for the entire collection you were looking at.
|
178
|
+
|
179
|
+
3) Each result set will have a JitPreloader setup on it, and the preloader will have a reference to all of the other objects in a result set. This means that so long as one object of that result set exists in memory, the others will not be cleaned up by the garbage collector. This shouldn't have much impact, but it's good to be aware of it.
|
180
|
+
|
181
|
+
## Contributing
|
182
|
+
|
183
|
+
1. Fork it ( https://github.com/[my-github-username]/jit_preloader/fork )
|
184
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
185
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
186
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
187
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jit_preloader/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jit_preloader"
|
8
|
+
spec.version = JitPreloader::VERSION
|
9
|
+
spec.authors = ["Kyle d'Oliveira"]
|
10
|
+
spec.email = ["kyle.doliveira@clio.com"]
|
11
|
+
spec.summary = %q{Tool to understand N+1 queries and to remove them}
|
12
|
+
spec.description = %q{The JitPreloader has the ability to send notifications when N+1 queries occur to help guage how problematic they are for your code base and a way to remove all of the commons explicitly or automatically}
|
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", "~> 4.2"
|
22
|
+
spec.add_dependency "activesupport"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "database_cleaner"
|
28
|
+
spec.add_development_dependency "sqlite3"
|
29
|
+
spec.add_development_dependency "byebug"
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class ActiveRecord::Associations::CollectionAssociation
|
2
|
+
|
3
|
+
def load_target_with_jit
|
4
|
+
was_loaded = loaded?
|
5
|
+
|
6
|
+
if !loaded? && owner.persisted? && owner.jit_preloader
|
7
|
+
owner.jit_preloader.jit_preload(reflection.name)
|
8
|
+
end
|
9
|
+
|
10
|
+
jit_loaded = loaded?
|
11
|
+
|
12
|
+
load_target_without_jit.tap do |records|
|
13
|
+
# We should not act on non-persisted objects, or ones that are already loaded.
|
14
|
+
if owner.persisted? && !was_loaded
|
15
|
+
# If we went through a JIT preload, then we will have attached another JitPreloader elsewhere.
|
16
|
+
JitPreloader::Preloader.attach(records) if records.any? && !jit_loaded && JitPreloader.globally_enabled?
|
17
|
+
|
18
|
+
# If the records were not pre_loaded
|
19
|
+
records.each{ |record| record.jit_n_plus_one_tracking = true }
|
20
|
+
|
21
|
+
if !jit_loaded && owner.jit_n_plus_one_tracking
|
22
|
+
ActiveSupport::Notifications.publish("n_plus_one_query",
|
23
|
+
source: owner, association: reflection.name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
alias_method_chain :load_target, :jit
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class ActiveRecord::Associations::Preloader::CollectionAssociation
|
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
|
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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class ActiveRecord::Associations::Preloader::SingularAssociation
|
2
|
+
private
|
3
|
+
# A monkey patch to ActiveRecord. The old method looked like the snippet
|
4
|
+
# below. Our changes here are that we don't assign the record if the
|
5
|
+
# target has already been set, and we attach all of the records to a new
|
6
|
+
# jit preloader.
|
7
|
+
#
|
8
|
+
# def preload(preloader)
|
9
|
+
# associated_records_by_owner(preloader).each do |owner, associated_records|
|
10
|
+
# record = associated_records.first
|
11
|
+
# association = owner.association(reflection.name)
|
12
|
+
# association.target = record
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
|
16
|
+
def preload(preloader)
|
17
|
+
return unless reflection.scope.nil? || reflection.scope.arity == 0
|
18
|
+
all_records = []
|
19
|
+
|
20
|
+
associated_records_by_owner(preloader).each do |owner, associated_records|
|
21
|
+
record = associated_records.first
|
22
|
+
|
23
|
+
association = owner.association(reflection.name)
|
24
|
+
association.target ||= record
|
25
|
+
all_records.push(record) if record && (owner.jit_preloader || JitPreloader.globally_enabled?)
|
26
|
+
end
|
27
|
+
JitPreloader::Preloader.attach(all_records) if all_records.any?
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class ActiveRecord::Associations::SingularAssociation
|
2
|
+
|
3
|
+
def load_target_with_jit
|
4
|
+
was_loaded = loaded?
|
5
|
+
|
6
|
+
if !loaded? && owner.persisted? && owner.jit_preloader
|
7
|
+
owner.jit_preloader.jit_preload(reflection.name)
|
8
|
+
end
|
9
|
+
|
10
|
+
jit_loaded = loaded?
|
11
|
+
|
12
|
+
load_target_without_jit.tap do |record|
|
13
|
+
if owner.persisted? && !was_loaded
|
14
|
+
# If the owner doesn't track N+1 queries, then we don't need to worry about
|
15
|
+
# tracking it on the record. This is because you can do something like:
|
16
|
+
# model.foo.bar (where foo and bar are singular associations) and that isn't
|
17
|
+
# always an N+1 query.
|
18
|
+
record.jit_n_plus_one_tracking ||= owner.jit_n_plus_one_tracking if record
|
19
|
+
|
20
|
+
if !jit_loaded && owner.jit_n_plus_one_tracking
|
21
|
+
ActiveSupport::Notifications.publish("n_plus_one_query",
|
22
|
+
source: owner, association: reflection.name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
alias_method_chain :load_target, :jit
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module JitPreloadExtension
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attr_accessor :jit_preloader
|
7
|
+
attr_accessor :jit_n_plus_one_tracking
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
delegate :jit_preload, to: :all
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveRecord::Base.send(:include, JitPreloadExtension)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class ActiveRecord::Relation
|
2
|
+
|
3
|
+
def calculate_with_jit(*args)
|
4
|
+
if respond_to?(:proxy_association) && proxy_association.owner && proxy_association.owner.jit_n_plus_one_tracking
|
5
|
+
ActiveSupport::Notifications.publish("n_plus_one_query",
|
6
|
+
source: proxy_association.owner,
|
7
|
+
association: "#{proxy_association.reflection.name}.#{args.first}")
|
8
|
+
end
|
9
|
+
calculate_without_jit(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method_chain :calculate, :jit
|
13
|
+
|
14
|
+
def exec_queries_with_jit
|
15
|
+
exec_queries_without_jit.tap do |records|
|
16
|
+
if limit_value != 1
|
17
|
+
records.each{ |record| record.jit_n_plus_one_tracking = true }
|
18
|
+
if jit_preload? || JitPreloader.globally_enabled?
|
19
|
+
JitPreloader::Preloader.attach(records)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
alias_method_chain :exec_queries, :jit
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module JitPreloader
|
2
|
+
class Preloader < ActiveRecord::Associations::Preloader
|
3
|
+
|
4
|
+
attr_accessor :records
|
5
|
+
|
6
|
+
def self.attach(records)
|
7
|
+
new.tap do |loader|
|
8
|
+
loader.records = records
|
9
|
+
records.each do |record|
|
10
|
+
record.jit_preloader = loader
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def jit_preload(association)
|
16
|
+
# It is possible that the records array has multiple different classes (think single table inheritance).
|
17
|
+
# Thus, it is possible that some of the records don't have an association.
|
18
|
+
records_with_association = records.reject{|r| r.class.reflect_on_association(association).nil? }
|
19
|
+
preload records_with_association, association
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
require 'active_support/notifications'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
require "jit_preloader/version"
|
7
|
+
require 'jit_preloader/active_record/base'
|
8
|
+
require 'jit_preloader/active_record/query_methods'
|
9
|
+
require 'jit_preloader/active_record/relation'
|
10
|
+
require 'jit_preloader/active_record/associations/collection_association'
|
11
|
+
require 'jit_preloader/active_record/associations/singular_association'
|
12
|
+
require 'jit_preloader/active_record/associations/preloader/collection_association'
|
13
|
+
require 'jit_preloader/active_record/associations/preloader/singular_association'
|
14
|
+
require 'jit_preloader/preloader'
|
15
|
+
|
16
|
+
module JitPreloader
|
17
|
+
|
18
|
+
def self.globally_enabled=(value)
|
19
|
+
@enabled = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.globally_enabled?
|
23
|
+
if @enabled && @enabled.respond_to?(:call)
|
24
|
+
@enabled.call
|
25
|
+
else
|
26
|
+
@enabled
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe JitPreloader::Preloader do
|
4
|
+
|
5
|
+
let!(:contact1) do
|
6
|
+
Contact.create(
|
7
|
+
name: "Only Addresses",
|
8
|
+
addresses: [
|
9
|
+
Address.new(street: "123 Fake st", country: canada),
|
10
|
+
Address.new(street: "21 Jump st", country: usa)
|
11
|
+
]
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
let!(:contact2) do
|
16
|
+
Contact.create(
|
17
|
+
name: "Only Emails",
|
18
|
+
email_address: EmailAddress.new(address: "woot@woot.com"),
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
let!(:contact3) do
|
23
|
+
Contact.create(
|
24
|
+
name: "Both!",
|
25
|
+
addresses: [
|
26
|
+
Address.new(street: "1 First st", country: canada),
|
27
|
+
Address.new(street: "10 Tenth Ave", country: usa)
|
28
|
+
],
|
29
|
+
email_address: EmailAddress.new(address: "woot@woot.com"),
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:canada) { Country.create(name: "Canada") }
|
34
|
+
let(:usa) { Country.create(name: "U.S.A") }
|
35
|
+
|
36
|
+
let(:source_map) { Hash.new{|h,k| h[k]= Array.new } }
|
37
|
+
let(:callback) do
|
38
|
+
->(event, data){ source_map[data[:source]] << data[:association] }
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when the preloader is globally enabled" do
|
42
|
+
around do |example|
|
43
|
+
JitPreloader.globally_enabled = true
|
44
|
+
example.run
|
45
|
+
JitPreloader.globally_enabled = false
|
46
|
+
end
|
47
|
+
context "when grabbing all of the address'es contries and email addresses" do
|
48
|
+
it "doesn't generate an N+1 query ntoification" do
|
49
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
50
|
+
Contact.all.collect{|c| c.addresses.collect(&:country); c.email_address }
|
51
|
+
end
|
52
|
+
expect(source_map).to eql({})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when we perform aggregate functions on the data" do
|
57
|
+
it "generates N+1 query notifications for each one" do
|
58
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
59
|
+
Contact.all.each{|c| c.addresses.count; c.addresses.sum(:id) }
|
60
|
+
end
|
61
|
+
contact_queries = [contact1,contact2, contact3].product([["addresses.count", "addresses.sum"]])
|
62
|
+
expect(source_map).to eql(Hash[contact_queries])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when the preloader is not globally enabled" do
|
68
|
+
context "when we perform aggregate functions on the data" do
|
69
|
+
it "generates N+1 query notifications for each one" do
|
70
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
71
|
+
Contact.all.each{|c| c.addresses.count; c.addresses.sum(:id) }
|
72
|
+
end
|
73
|
+
contact_queries = [contact1,contact2, contact3].product([["addresses.count", "addresses.sum"]])
|
74
|
+
expect(source_map).to eql(Hash[contact_queries])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when explicitly finding a contact" do
|
79
|
+
it "generates N+1 query notifications for the country" do
|
80
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
81
|
+
Contact.find(contact1.id).tap{|c| c.addresses.collect(&:country); c.email_address }
|
82
|
+
end
|
83
|
+
address_queries = Address.where(contact_id: 1).product([[:country]])
|
84
|
+
expect(source_map).to eql(Hash[address_queries])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when explicitly finding multiple contacts" do
|
89
|
+
it "generates N+1 query notifications for the country" do
|
90
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
91
|
+
Contact.find(contact1.id, contact2.id).each{|c| c.addresses.collect(&:country); c.email_address }
|
92
|
+
end
|
93
|
+
contact_queries = [contact1,contact2].product([[:addresses, :email_address]])
|
94
|
+
address_queries = Address.where(contact_id: contact1.id).product([[:country]])
|
95
|
+
|
96
|
+
expect(source_map).to eql(Hash[address_queries.concat(contact_queries)])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when grabbing the email address and address's country of the first contact" do
|
101
|
+
it "generates N+1 query notifications for the country" do
|
102
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
103
|
+
Contact.first.tap{|c| c.addresses.collect(&:country); c.email_address }
|
104
|
+
end
|
105
|
+
|
106
|
+
address_queries = Address.where(contact_id: contact1.id).product([[:country]])
|
107
|
+
|
108
|
+
expect(source_map).to eql(Hash[address_queries])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when grabbing all of the address'es contries and email addresses" do
|
113
|
+
it "generates an N+1 query for each association on the contacts" do
|
114
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
115
|
+
Contact.all.each{|c| c.addresses.collect(&:country); c.email_address }
|
116
|
+
end
|
117
|
+
contact_queries = [contact1,contact2,contact3].product([[:addresses, :email_address]])
|
118
|
+
address_queries = Address.all.product([[:country]])
|
119
|
+
expect(source_map).to eql(Hash[address_queries.concat(contact_queries)])
|
120
|
+
end
|
121
|
+
|
122
|
+
context "and we use regular preload for addresses" do
|
123
|
+
it "generates an N+1 query for only the email addresses on the contacts" do
|
124
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
125
|
+
Contact.preload(:addresses).each{|c| c.addresses.collect(&:country); c.email_address }
|
126
|
+
end
|
127
|
+
contact_queries = [contact1,contact2,contact3].product([[:email_address]])
|
128
|
+
address_queries = Address.all.product([[:country]])
|
129
|
+
expect(source_map).to eql(Hash[address_queries.concat(contact_queries)])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "and we use jit preload" do
|
134
|
+
it "generates no n+1 queries" do
|
135
|
+
ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
|
136
|
+
Contact.jit_preload.each{|c| c.addresses.collect(&:country); c.email_address }
|
137
|
+
end
|
138
|
+
expect(source_map).to eql({})
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require 'jit_preloader'
|
5
|
+
require 'support/database'
|
6
|
+
require 'support/models'
|
7
|
+
require 'byebug'
|
8
|
+
require 'database_cleaner'
|
9
|
+
|
10
|
+
DatabaseCleaner.strategy = :transaction
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.before(:suite) do
|
14
|
+
Database.connect!
|
15
|
+
Database.build!
|
16
|
+
end
|
17
|
+
config.before do
|
18
|
+
DatabaseCleaner.start
|
19
|
+
end
|
20
|
+
|
21
|
+
config.after do
|
22
|
+
DatabaseCleaner.clean
|
23
|
+
end
|
24
|
+
|
25
|
+
# rspec-expectations config goes here. You can use an alternate
|
26
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
27
|
+
# assertions if you prefer.
|
28
|
+
config.expect_with :rspec do |expectations|
|
29
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
30
|
+
# and `failure_message` of custom matchers include text for helper methods
|
31
|
+
# defined using `chain`, e.g.:
|
32
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
33
|
+
# # => "be bigger than 2 and smaller than 4"
|
34
|
+
# ...rather than:
|
35
|
+
# # => "be bigger than 2"
|
36
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
37
|
+
end
|
38
|
+
|
39
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
40
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
41
|
+
config.mock_with :rspec do |mocks|
|
42
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
43
|
+
# a real object. This is generally recommended, and will default to
|
44
|
+
# `true` in RSpec 4.
|
45
|
+
mocks.verify_partial_doubles = true
|
46
|
+
end
|
47
|
+
|
48
|
+
config.disable_monkey_patching!
|
49
|
+
|
50
|
+
config.order = :random
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Database
|
2
|
+
def self.tables
|
3
|
+
[
|
4
|
+
"CREATE TABLE contacts (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
5
|
+
"CREATE TABLE addresses (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, country_id INTEGER NOT NULL, street VARCHAR(255))",
|
6
|
+
"CREATE TABLE email_addresses (id INTEGER NOT NULL PRIMARY KEY, contact_id INTEGER NOT NULL, address VARCHAR(255))",
|
7
|
+
"CREATE TABLE countries (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255))",
|
8
|
+
]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.build!
|
12
|
+
tables.each do |table|
|
13
|
+
ActiveRecord::Base.connection.execute(table)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.connect!
|
18
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ":memory:"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Contact < ActiveRecord::Base
|
2
|
+
has_many :addresses
|
3
|
+
has_one :email_address
|
4
|
+
end
|
5
|
+
|
6
|
+
class Address < ActiveRecord::Base
|
7
|
+
belongs_to :contact
|
8
|
+
belongs_to :country
|
9
|
+
end
|
10
|
+
|
11
|
+
class EmailAddress < ActiveRecord::Base
|
12
|
+
belongs_to :contact
|
13
|
+
end
|
14
|
+
|
15
|
+
class Country < ActiveRecord::Base
|
16
|
+
has_many :addresses
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jit_preloader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kyle d'Oliveira
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-31 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: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
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: database_cleaner
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
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: byebug
|
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
|
+
description: The JitPreloader has the ability to send notifications when N+1 queries
|
126
|
+
occur to help guage how problematic they are for your code base and a way to remove
|
127
|
+
all of the commons explicitly or automatically
|
128
|
+
email:
|
129
|
+
- kyle.doliveira@clio.com
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- ".gitignore"
|
135
|
+
- ".rspec"
|
136
|
+
- Gemfile
|
137
|
+
- Gemfile.lock
|
138
|
+
- LICENSE
|
139
|
+
- README.md
|
140
|
+
- Rakefile
|
141
|
+
- jit_preloader.gemspec
|
142
|
+
- lib/jit_preloader.rb
|
143
|
+
- lib/jit_preloader/active_record/associations/collection_association.rb
|
144
|
+
- lib/jit_preloader/active_record/associations/preloader/collection_association.rb
|
145
|
+
- lib/jit_preloader/active_record/associations/preloader/singular_association.rb
|
146
|
+
- lib/jit_preloader/active_record/associations/singular_association.rb
|
147
|
+
- lib/jit_preloader/active_record/base.rb
|
148
|
+
- lib/jit_preloader/active_record/query_methods.rb
|
149
|
+
- lib/jit_preloader/active_record/relation.rb
|
150
|
+
- lib/jit_preloader/preloader.rb
|
151
|
+
- lib/jit_preloader/version.rb
|
152
|
+
- spec/lib/jit_preloader/preloader_spec.rb
|
153
|
+
- spec/spec_helper.rb
|
154
|
+
- spec/support/database.rb
|
155
|
+
- spec/support/models.rb
|
156
|
+
homepage: ''
|
157
|
+
licenses:
|
158
|
+
- MIT
|
159
|
+
metadata: {}
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options: []
|
162
|
+
require_paths:
|
163
|
+
- lib
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
requirements: []
|
175
|
+
rubyforge_project:
|
176
|
+
rubygems_version: 2.4.3
|
177
|
+
signing_key:
|
178
|
+
specification_version: 4
|
179
|
+
summary: Tool to understand N+1 queries and to remove them
|
180
|
+
test_files:
|
181
|
+
- spec/lib/jit_preloader/preloader_spec.rb
|
182
|
+
- spec/spec_helper.rb
|
183
|
+
- spec/support/database.rb
|
184
|
+
- spec/support/models.rb
|