jit_preloader 0.1.0 → 0.2.0

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