jit_preloader 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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