predictive_load 0.4.0 → 0.5.1

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
- SHA1:
3
- metadata.gz: e90bbf6f94ab919853192241e22b311e67202681
4
- data.tar.gz: e7606fd33878084afdcd62ca317d428b62cd5b24
2
+ SHA256:
3
+ metadata.gz: 4b0c3352a1804088cd709d17e619ff406cc243a92ef5b180815416e704b89775
4
+ data.tar.gz: b12b98881fccee776a2cf2175a8c2d4c06eb6325d1d5a47c08a732f5b6c2eb51
5
5
  SHA512:
6
- metadata.gz: 9c5344e844ebe9295b7865681dc1d7c91c2331fc180187d1f2a1a4da7da5784010dc2ccb1d565c3f939b6c6683199ee5a1e1591dcd696196ceed2b4829964fcd
7
- data.tar.gz: 09d21270213b7c416d7fd8a18ff238ad61d41d1616d2fb1274b22670223d9a7c68da2bb0995375ff45b75e102f9449f50ccc4dd4ae7ebd00e377d177ce966049
6
+ metadata.gz: a67ac4027e5b1f8c5d70b4799f7cbd230c18c0b18a2100085c1876a97ece7b283b621a22981ac5f1e0d16e6c0f6383746d436080f57a2b1810a216230c20a74e
7
+ data.tar.gz: baa04313709dd51cbe4269a9cf536559d88ea24d55409a0ba80dd9f1f1db9fa4b1f4ee03df9df57841bb5abe81250255bbb9b257d55fcd6e9f653b5dbc2479d2
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- [![Build Status](https://travis-ci.org/eac/predictive_load.png)](https://travis-ci.org/eac/predictive_load)
1
+ [![Build Status](https://github.com/zendesk/predictive_load/actions/workflows/actions.yml/badge.svg?branch=master)](https://github.com/zendesk/predictive_load/actions/workflows/actions.yml)
2
+
2
3
 
3
4
  predictive_load
4
5
  ===============
@@ -40,40 +41,6 @@ Some things cannot be preloaded, use `predictive_load: false`
40
41
  has_many :foos, predictive_load: false
41
42
  ```
42
43
 
43
- ### N+1 detection logging
44
-
45
- There is also a log-only version:
46
- ```ruby
47
- require 'predictive_load'
48
- require 'predictive_load/active_record_collection_observation'
49
- ActiveRecord::Base.send(:include, PredictiveLoad::ActiveRecordCollectionObservation)
50
-
51
- require 'predictive_load/watcher'
52
-
53
- ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
54
-
55
- Comment.all.each do |comment|
56
- comment.account
57
- end
58
-
59
- ```
60
-
61
- Produces:
62
-
63
- ```
64
- detected n1 call on Comment#account
65
- expect to prevent 10 queries
66
- would preload with: SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (...)
67
- +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
68
- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
69
- +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
70
- | 1 | SIMPLE | accounts | const | PRIMARY | PRIMARY | 4 | const | 10 | |
71
- +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
72
- 1 row in set (0.00 sec)
73
- would have prevented all 10 queries
74
-
75
- ```
76
-
77
44
  #### Known limitations:
78
45
 
79
46
  * Calling association#size will trigger an N+1 on SELECT COUNT(*). Work around by calling #length, loading all records.
@@ -1,31 +1,40 @@
1
1
  module PredictiveLoad::ActiveRecordCollectionObservation
2
2
 
3
3
  def self.included(base)
4
- ActiveRecord::Relation.send(:include, RelationObservation)
5
- ActiveRecord::Base.send(:include, CollectionMember)
6
- ActiveRecord::Base.send(:extend, UnscopedTracker)
7
- ActiveRecord::Associations::Association.send(:include, AssociationNotification)
8
- ActiveRecord::Associations::CollectionAssociation.send(:include, CollectionAssociationNotification)
4
+ ActiveRecord::Relation.class_attribute :collection_observer
5
+ if ActiveRecord::VERSION::MAJOR >= 5
6
+ ActiveRecord::Relation.prepend Rails5RelationObservation
7
+ else
8
+ ActiveRecord::Relation.prepend Rails4RelationObservation
9
+ end
10
+ ActiveRecord::Base.include CollectionMember
11
+ ActiveRecord::Base.extend UnscopedTracker
12
+ ActiveRecord::Associations::Association.prepend AssociationNotification
13
+ ActiveRecord::Associations::CollectionAssociation.prepend CollectionAssociationNotification
9
14
  end
10
15
 
11
- module RelationObservation
12
-
13
- def self.included(base)
14
- base.class_attribute :collection_observer
15
- base.send(:alias_method, :to_a_without_collection_observer, :to_a)
16
- base.send(:alias_method, :to_a, :to_a_with_collection_observer)
16
+ module Rails5RelationObservation
17
+ # this essentially intercepts the enumerable methods that would result in n+1s since most of
18
+ # those are delegated to :records in Rails 5+ in the ActiveRecord::Relation::Delegation module
19
+ def records
20
+ record_array = super
21
+ if record_array.size > 1 && collection_observer
22
+ collection_observer.observe(record_array.dup)
23
+ end
24
+ record_array
17
25
  end
26
+ end
18
27
 
19
- def to_a_with_collection_observer
20
- records = to_a_without_collection_observer
21
-
22
- if records.size > 1 && collection_observer
23
- collection_observer.observe(records.dup)
28
+ module Rails4RelationObservation
29
+ # this essentially intercepts the enumerable methods that would result in n+1s since most of
30
+ # those are delegated to :to_a in Rails 5+ in the ActiveRecord::Relation::Delegation module
31
+ def to_a
32
+ record_array = super
33
+ if record_array.size > 1 && collection_observer
34
+ collection_observer.observe(record_array.dup)
24
35
  end
25
-
26
- records
36
+ record_array
27
37
  end
28
-
29
38
  end
30
39
 
31
40
  module CollectionMember
@@ -57,16 +66,10 @@ module PredictiveLoad::ActiveRecordCollectionObservation
57
66
  end
58
67
 
59
68
  module AssociationNotification
60
-
61
- def self.included(base)
62
- base.send(:alias_method, :load_target_without_notification, :load_target)
63
- base.send(:alias_method, :load_target, :load_target_with_notification)
64
- end
65
-
66
- def load_target_with_notification
69
+ def load_target
67
70
  notify_collection_observer if find_target?
68
71
 
69
- load_target_without_notification
72
+ super
70
73
  end
71
74
 
72
75
  protected
@@ -76,22 +79,14 @@ module PredictiveLoad::ActiveRecordCollectionObservation
76
79
  @owner.collection_observer.loading_association(@owner, self)
77
80
  end
78
81
  end
79
-
80
82
  end
81
83
 
82
84
  module CollectionAssociationNotification
83
-
84
- def self.included(base)
85
- base.send(:alias_method, :load_target_without_notification, :load_target)
86
- base.send(:alias_method, :load_target, :load_target_with_notification)
87
- end
88
-
89
- def load_target_with_notification
85
+ def load_target
90
86
  notify_collection_observer if find_target?
91
87
 
92
- load_target_without_notification
88
+ super
93
89
  end
94
-
95
90
  end
96
91
 
97
92
  end
@@ -57,10 +57,21 @@ module PredictiveLoad
57
57
  end
58
58
 
59
59
  def preload(association_name)
60
+ # https://github.com/rails/rails/blob/v4.2.10/activerecord/lib/active_record/associations/preloader.rb#L187 (similar to other Rails versions)
61
+ # If the first record association is loaded, Preloader aborts.
62
+ #
63
+ # In a code like `comments.each { |c| c.user }, if the first comment user_id is nil,
64
+ # when calling the method (`user`) ActiveRecord doesn't load the association, but marks it as loaded.
65
+ # So when the second comment calls `user` (and user_id is not nil), @records.first will be the first
66
+ # comment above (with thr association already loaded), which will be checked by Preloader and used to skip
67
+ # any preloading.
68
+ #
69
+ # Fix is pretty simple, ignore any record with association already loaded.
70
+ rs = records_with_association(association_name).reject { |r| r.association(association_name).loaded? }
60
71
  if ActiveRecord::VERSION::STRING <= "4.1.0"
61
- ActiveRecord::Associations::Preloader.new(records_with_association(association_name), [ association_name ]).run
72
+ ActiveRecord::Associations::Preloader.new(rs, [ association_name ]).run
62
73
  else
63
- ActiveRecord::Associations::Preloader.new.preload(records_with_association(association_name), [ association_name ])
74
+ ActiveRecord::Associations::Preloader.new.preload(rs, [ association_name ])
64
75
  end
65
76
  end
66
77
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: predictive_load
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Chapweske
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-12 00:00:00.000000000 Z
11
+ date: 2021-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,132 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.2.0
19
+ version: 4.2.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '5.1'
22
+ version: '5.3'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 3.2.0
29
+ version: 4.2.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '5.1'
33
- - !ruby/object:Gem::Dependency
34
- name: minitest
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
- type: :development
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- - !ruby/object:Gem::Dependency
48
- name: minitest-rg
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- - !ruby/object:Gem::Dependency
62
- name: sqlite3
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
- - !ruby/object:Gem::Dependency
76
- name: rake
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
- type: :development
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: '0'
89
- - !ruby/object:Gem::Dependency
90
- name: bump
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- version: '0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: '0'
103
- - !ruby/object:Gem::Dependency
104
- name: wwtd
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
117
- - !ruby/object:Gem::Dependency
118
- name: query_diet
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
122
- - !ruby/object:Gem::Version
123
- version: '0'
124
- type: :development
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- version: '0'
131
- - !ruby/object:Gem::Dependency
132
- name: byebug
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'
32
+ version: '5.3'
145
33
  description: Predictive loader
146
34
  email:
147
35
  - eac@zendesk.com
@@ -155,12 +43,11 @@ files:
155
43
  - lib/predictive_load/active_record_collection_observation.rb
156
44
  - lib/predictive_load/loader.rb
157
45
  - lib/predictive_load/preload_log.rb
158
- - lib/predictive_load/watcher.rb
159
- homepage: ''
46
+ homepage: https://github.com/zendesk/predictive_load
160
47
  licenses:
161
48
  - Apache License Version 2.0
162
49
  metadata: {}
163
- post_install_message:
50
+ post_install_message:
164
51
  rdoc_options: []
165
52
  require_paths:
166
53
  - lib
@@ -168,16 +55,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
55
  requirements:
169
56
  - - ">="
170
57
  - !ruby/object:Gem::Version
171
- version: '0'
58
+ version: '2.4'
172
59
  required_rubygems_version: !ruby/object:Gem::Requirement
173
60
  requirements:
174
61
  - - ">="
175
62
  - !ruby/object:Gem::Version
176
63
  version: '0'
177
64
  requirements: []
178
- rubyforge_project:
179
- rubygems_version: 2.4.5.1
180
- signing_key:
65
+ rubyforge_project:
66
+ rubygems_version: 2.7.6.2
67
+ signing_key:
181
68
  specification_version: 4
182
69
  summary: ''
183
70
  test_files: []
@@ -1,84 +0,0 @@
1
- raise "Not supported on rails 4.1+" if ActiveRecord::VERSION::STRING >= "4.1.0"
2
-
3
- require 'predictive_load/loader'
4
- require 'predictive_load/preload_log'
5
-
6
- module PredictiveLoad
7
- # Provides N+1 detection / log mode.
8
- #
9
- # Usage:
10
- # ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
11
- #
12
- # Example output:
13
- # predictive_load: detected n1 call on Comment#account
14
- # predictive_load: expect to prevent 1 queries
15
- # predictive_load: would preload with: SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (...)
16
- # predictive_load: +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
17
- # predictive_load: | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
18
- # predictive_load: +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
19
- # predictive_load: | 1 | SIMPLE | accounts | const | PRIMARY | PRIMARY | 4 | const | 1 | |
20
- # predictive_load: +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
21
- # predictive_load: 1 row in set (0.00 sec)
22
- # predictive_load: would have prevented all 1 queries
23
- class Watcher < Loader
24
-
25
- attr_reader :loaded_associations
26
-
27
- def initialize(records)
28
- super
29
- @loaded_associations = {}
30
- end
31
-
32
- def loading_association(record, association)
33
- association_name = association.reflection.name
34
- return if !all_records_will_likely_load_association?(association_name)
35
- return if !supports_preload?(association)
36
-
37
- if loaded_associations.key?(association_name)
38
- log_query_plan(association_name)
39
- end
40
-
41
- increment_query_count(association_name)
42
- end
43
-
44
- protected
45
-
46
- def log_query_plan(association_name)
47
- log("detected n+1 call on #{records.first.class.name}##{association_name}")
48
-
49
- # Detailed logging for first query
50
- if query_count(association_name) == 1
51
- log("expect to prevent #{expected_query_count} queries")
52
- log_preload(association_name)
53
- end
54
-
55
- # All records loaded association
56
- if query_count(association_name) == expected_query_count
57
- log("would have prevented all #{expected_query_count} queries")
58
- end
59
-
60
- end
61
-
62
- def query_count(association_name)
63
- loaded_associations[association_name] || 0
64
- end
65
-
66
- def increment_query_count(association_name)
67
- loaded_associations[association_name] ||= 0
68
- loaded_associations[association_name] += 1
69
- end
70
-
71
- def expected_query_count
72
- records.size - 1
73
- end
74
-
75
- def log_preload(association_name)
76
- PreloadLog.new(records_with_association(association_name), [ association_name ]).run
77
- end
78
-
79
- def log(message)
80
- ActiveRecord::Base.logger.info("predictive_load: #{message}")
81
- end
82
-
83
- end
84
- end