bullet 4.13.1 → 5.5.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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +7 -11
- data/CHANGELOG.md +62 -3
- data/Gemfile.mongoid +0 -2
- data/Gemfile.mongoid-4.0 +1 -5
- data/{Gemfile.mongoid-2.8 → Gemfile.mongoid-5.0} +2 -6
- data/{Gemfile.mongoid-2.5 → Gemfile.mongoid-6.0} +2 -6
- data/Gemfile.rails-4.0 +2 -5
- data/Gemfile.rails-4.1 +2 -5
- data/{Gemfile.rails-3.0 → Gemfile.rails-4.2} +3 -6
- data/{Gemfile.rails-3.1 → Gemfile.rails-5.0} +2 -6
- data/Hacking.md +1 -0
- data/README.md +39 -7
- data/bullet.gemspec +1 -1
- data/lib/bullet/active_record4.rb +109 -33
- data/lib/bullet/active_record41.rb +99 -37
- data/lib/bullet/active_record42.rb +251 -0
- data/lib/bullet/active_record5.rb +243 -0
- data/lib/bullet/dependency.rb +34 -28
- data/lib/bullet/detector/association.rb +16 -16
- data/lib/bullet/detector/counter_cache.rb +13 -13
- data/lib/bullet/detector/n_plus_one_query.rb +30 -35
- data/lib/bullet/detector/unused_eager_loading.rb +12 -11
- data/lib/bullet/ext/object.rb +4 -2
- data/lib/bullet/{mongoid3x.rb → mongoid5x.rb} +3 -3
- data/lib/bullet/{mongoid2x.rb → mongoid6x.rb} +12 -12
- data/lib/bullet/notification/base.rb +9 -13
- data/lib/bullet/notification/n_plus_one_query.rb +8 -6
- data/lib/bullet/notification/unused_eager_loading.rb +18 -1
- data/lib/bullet/rack.rb +28 -16
- data/lib/bullet/stack_trace_filter.rb +34 -0
- data/lib/bullet/version.rb +1 -1
- data/lib/bullet.rb +48 -14
- data/spec/bullet/detector/counter_cache_spec.rb +8 -8
- data/spec/bullet/detector/n_plus_one_query_spec.rb +46 -31
- data/spec/bullet/detector/unused_eager_loading_spec.rb +17 -3
- data/spec/bullet/ext/object_spec.rb +6 -0
- data/spec/bullet/notification/base_spec.rb +4 -29
- data/spec/bullet/notification/n_plus_one_query_spec.rb +4 -4
- data/spec/bullet/notification/unused_eager_loading_spec.rb +3 -3
- data/spec/bullet/rack_spec.rb +38 -7
- data/spec/bullet_spec.rb +93 -0
- data/spec/integration/active_record4/association_spec.rb +112 -4
- data/spec/integration/{active_record3 → active_record5}/association_spec.rb +121 -15
- data/spec/integration/counter_cache_spec.rb +24 -1
- data/spec/models/category.rb +4 -1
- data/spec/models/comment.rb +2 -0
- data/spec/models/post.rb +7 -2
- data/spec/models/reply.rb +3 -0
- data/spec/models/submission.rb +1 -1
- data/spec/spec_helper.rb +3 -5
- data/spec/support/mongo_seed.rb +13 -0
- data/spec/support/sqlite_seed.rb +15 -6
- data/test.sh +4 -10
- data/update.sh +7 -0
- metadata +21 -23
- data/Gemfile.mongoid-2.4 +0 -19
- data/Gemfile.mongoid-2.6 +0 -19
- data/Gemfile.mongoid-2.7 +0 -19
- data/Gemfile.mongoid-3.0 +0 -19
- data/Gemfile.mongoid-3.1 +0 -19
- data/Gemfile.rails-3.2 +0 -19
- data/lib/bullet/active_record3.rb +0 -154
- data/lib/bullet/active_record3x.rb +0 -134
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module SaveWithBulletSupport
|
|
3
|
+
def save(*args)
|
|
4
|
+
was_new_record = new_record?
|
|
5
|
+
super(*args).tap do |result|
|
|
6
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(self) if result && was_new_record
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def save!(*args)
|
|
11
|
+
was_new_record = new_record?
|
|
12
|
+
super(*args).tap do |result|
|
|
13
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(self) if result && was_new_record
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ActiveRecord
|
|
19
|
+
def self.enable
|
|
20
|
+
require 'active_record'
|
|
21
|
+
::ActiveRecord::Base.class_eval do
|
|
22
|
+
class <<self
|
|
23
|
+
alias_method :origin_find_by_sql, :find_by_sql
|
|
24
|
+
def find_by_sql(sql, binds = [], preparable: nil)
|
|
25
|
+
result = origin_find_by_sql(sql, binds, preparable: nil)
|
|
26
|
+
if Bullet.start?
|
|
27
|
+
if result.is_a? Array
|
|
28
|
+
if result.size > 1
|
|
29
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
|
|
30
|
+
Bullet::Detector::CounterCache.add_possible_objects(result)
|
|
31
|
+
elsif result.size == 1
|
|
32
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
|
|
33
|
+
Bullet::Detector::CounterCache.add_impossible_object(result.first)
|
|
34
|
+
end
|
|
35
|
+
elsif result.is_a? ::ActiveRecord::Base
|
|
36
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
|
|
37
|
+
Bullet::Detector::CounterCache.add_impossible_object(result)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
|
|
46
|
+
|
|
47
|
+
::ActiveRecord::Relation.class_eval do
|
|
48
|
+
alias_method :origin_records, :records
|
|
49
|
+
# if select a collection of objects, then these objects have possible to cause N+1 query.
|
|
50
|
+
# if select only one object, then the only one object has impossible to cause N+1 query.
|
|
51
|
+
def records
|
|
52
|
+
result = origin_records
|
|
53
|
+
if Bullet.start?
|
|
54
|
+
if result.first.class.name !~ /^HABTM_/
|
|
55
|
+
if result.size > 1
|
|
56
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
|
|
57
|
+
Bullet::Detector::CounterCache.add_possible_objects(result)
|
|
58
|
+
elsif result.size == 1
|
|
59
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
|
|
60
|
+
Bullet::Detector::CounterCache.add_impossible_object(result.first)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
::ActiveRecord::Associations::Preloader.class_eval do
|
|
69
|
+
alias_method :origin_preloaders_for_one, :preloaders_for_one
|
|
70
|
+
|
|
71
|
+
def preloaders_for_one(association, records, scope)
|
|
72
|
+
if Bullet.start?
|
|
73
|
+
records.compact!
|
|
74
|
+
if records.first.class.name !~ /^HABTM_/
|
|
75
|
+
records.each do |record|
|
|
76
|
+
Bullet::Detector::Association.add_object_associations(record, association)
|
|
77
|
+
end
|
|
78
|
+
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
origin_preloaders_for_one(association, records, scope)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
::ActiveRecord::FinderMethods.class_eval do
|
|
86
|
+
# add includes in scope
|
|
87
|
+
alias_method :origin_find_with_associations, :find_with_associations
|
|
88
|
+
def find_with_associations
|
|
89
|
+
return origin_find_with_associations { |r| yield r } if block_given?
|
|
90
|
+
records = origin_find_with_associations
|
|
91
|
+
if Bullet.start?
|
|
92
|
+
associations = (eager_load_values + includes_values).uniq
|
|
93
|
+
records.each do |record|
|
|
94
|
+
Bullet::Detector::Association.add_object_associations(record, associations)
|
|
95
|
+
end
|
|
96
|
+
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
|
|
97
|
+
end
|
|
98
|
+
records
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
::ActiveRecord::Associations::JoinDependency.class_eval do
|
|
103
|
+
alias_method :origin_instantiate, :instantiate
|
|
104
|
+
alias_method :origin_construct, :construct
|
|
105
|
+
alias_method :origin_construct_model, :construct_model
|
|
106
|
+
|
|
107
|
+
def instantiate(result_set, aliases)
|
|
108
|
+
@bullet_eager_loadings = {}
|
|
109
|
+
records = origin_instantiate(result_set, aliases)
|
|
110
|
+
|
|
111
|
+
if Bullet.start?
|
|
112
|
+
@bullet_eager_loadings.each do |klazz, eager_loadings_hash|
|
|
113
|
+
objects = eager_loadings_hash.keys
|
|
114
|
+
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
records
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
|
|
121
|
+
if Bullet.start?
|
|
122
|
+
unless ar_parent.nil?
|
|
123
|
+
parent.children.each do |node|
|
|
124
|
+
key = aliases.column_alias(node, node.primary_key)
|
|
125
|
+
id = row[key]
|
|
126
|
+
if id.nil?
|
|
127
|
+
associations = node.reflection.name
|
|
128
|
+
Bullet::Detector::Association.add_object_associations(ar_parent, associations)
|
|
129
|
+
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, associations)
|
|
130
|
+
@bullet_eager_loadings[ar_parent.class] ||= {}
|
|
131
|
+
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
|
|
132
|
+
@bullet_eager_loadings[ar_parent.class][ar_parent] << associations
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
origin_construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# call join associations
|
|
142
|
+
def construct_model(record, node, row, model_cache, id, aliases)
|
|
143
|
+
result = origin_construct_model(record, node, row, model_cache, id, aliases)
|
|
144
|
+
|
|
145
|
+
if Bullet.start?
|
|
146
|
+
associations = node.reflection.name
|
|
147
|
+
Bullet::Detector::Association.add_object_associations(record, associations)
|
|
148
|
+
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
|
|
149
|
+
@bullet_eager_loadings[record.class] ||= {}
|
|
150
|
+
@bullet_eager_loadings[record.class][record] ||= Set.new
|
|
151
|
+
@bullet_eager_loadings[record.class][record] << associations
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
result
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
::ActiveRecord::Associations::CollectionAssociation.class_eval do
|
|
159
|
+
# call one to many associations
|
|
160
|
+
alias_method :origin_load_target, :load_target
|
|
161
|
+
def load_target
|
|
162
|
+
records = origin_load_target
|
|
163
|
+
|
|
164
|
+
if Bullet.start?
|
|
165
|
+
if self.is_a? ::ActiveRecord::Associations::ThroughAssociation
|
|
166
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
|
|
167
|
+
association = self.owner.association self.through_reflection.name
|
|
168
|
+
Array(association.target).each do |through_record|
|
|
169
|
+
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
|
|
173
|
+
if records.first.class.name !~ /^HABTM_/
|
|
174
|
+
if records.size > 1
|
|
175
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
|
|
176
|
+
Bullet::Detector::CounterCache.add_possible_objects(records)
|
|
177
|
+
elsif records.size == 1
|
|
178
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
|
|
179
|
+
Bullet::Detector::CounterCache.add_impossible_object(records.first)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
records
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
alias_method :origin_empty?, :empty?
|
|
187
|
+
def empty?
|
|
188
|
+
if Bullet.start? && !reflection.has_cached_counter?
|
|
189
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
|
190
|
+
end
|
|
191
|
+
origin_empty?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
alias_method :origin_include?, :include?
|
|
195
|
+
def include?(object)
|
|
196
|
+
if Bullet.start?
|
|
197
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
|
198
|
+
end
|
|
199
|
+
origin_include?(object)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
::ActiveRecord::Associations::SingularAssociation.class_eval do
|
|
204
|
+
# call has_one and belongs_to associations
|
|
205
|
+
alias_method :origin_reader, :reader
|
|
206
|
+
def reader(force_reload = false)
|
|
207
|
+
result = origin_reader(force_reload)
|
|
208
|
+
if Bullet.start?
|
|
209
|
+
if owner.class.name !~ /^HABTM_/ && !@inversed
|
|
210
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
|
211
|
+
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
|
|
212
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
|
|
213
|
+
else
|
|
214
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
result
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
::ActiveRecord::Associations::HasManyAssociation.class_eval do
|
|
223
|
+
alias_method :origin_many_empty?, :empty?
|
|
224
|
+
def empty?
|
|
225
|
+
result = origin_many_empty?
|
|
226
|
+
if Bullet.start? && !reflection.has_cached_counter?
|
|
227
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
|
228
|
+
end
|
|
229
|
+
result
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
alias_method :origin_count_records, :count_records
|
|
233
|
+
def count_records
|
|
234
|
+
result = reflection.has_cached_counter?
|
|
235
|
+
if Bullet.start? && !result && !self.is_a?(::ActiveRecord::Associations::ThroughAssociation)
|
|
236
|
+
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
|
|
237
|
+
end
|
|
238
|
+
origin_count_records
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
data/lib/bullet/dependency.rb
CHANGED
|
@@ -14,48 +14,42 @@ module Bullet
|
|
|
14
14
|
|
|
15
15
|
def active_record_version
|
|
16
16
|
@active_record_version ||= begin
|
|
17
|
-
if
|
|
18
|
-
'active_record3'
|
|
19
|
-
elsif active_record31? || active_record32?
|
|
20
|
-
'active_record3x'
|
|
21
|
-
elsif active_record40?
|
|
17
|
+
if active_record40?
|
|
22
18
|
'active_record4'
|
|
23
19
|
elsif active_record41?
|
|
24
20
|
'active_record41'
|
|
21
|
+
elsif active_record42?
|
|
22
|
+
'active_record42'
|
|
23
|
+
elsif active_record50?
|
|
24
|
+
'active_record5'
|
|
25
|
+
elsif active_record51?
|
|
26
|
+
'active_record5'
|
|
27
|
+
else
|
|
28
|
+
raise "Bullet does not support active_record #{::ActiveRecord::VERSION} yet"
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def mongoid_version
|
|
30
34
|
@mongoid_version ||= begin
|
|
31
|
-
if
|
|
32
|
-
'mongoid2x'
|
|
33
|
-
elsif mongoid3x?
|
|
34
|
-
'mongoid3x'
|
|
35
|
-
elsif mongoid4x?
|
|
35
|
+
if mongoid4x?
|
|
36
36
|
'mongoid4x'
|
|
37
|
+
elsif mongoid5x?
|
|
38
|
+
'mongoid5x'
|
|
39
|
+
elsif mongoid6x?
|
|
40
|
+
'mongoid6x'
|
|
41
|
+
else
|
|
42
|
+
raise "Bullet does not support mongoid #{::Mongoid::VERSION} yet"
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
41
|
-
def active_record3?
|
|
42
|
-
active_record? && ::ActiveRecord::VERSION::MAJOR == 3
|
|
43
|
-
end
|
|
44
|
-
|
|
45
47
|
def active_record4?
|
|
46
48
|
active_record? && ::ActiveRecord::VERSION::MAJOR == 4
|
|
47
49
|
end
|
|
48
50
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def active_record31?
|
|
54
|
-
active_record3? && ::ActiveRecord::VERSION::MINOR == 1
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def active_record32?
|
|
58
|
-
active_record3? && ::ActiveRecord::VERSION::MINOR == 2
|
|
51
|
+
def active_record5?
|
|
52
|
+
active_record? && ::ActiveRecord::VERSION::MAJOR == 5
|
|
59
53
|
end
|
|
60
54
|
|
|
61
55
|
def active_record40?
|
|
@@ -66,16 +60,28 @@ module Bullet
|
|
|
66
60
|
active_record4? && ::ActiveRecord::VERSION::MINOR == 1
|
|
67
61
|
end
|
|
68
62
|
|
|
69
|
-
def
|
|
70
|
-
|
|
63
|
+
def active_record42?
|
|
64
|
+
active_record4? && ::ActiveRecord::VERSION::MINOR == 2
|
|
71
65
|
end
|
|
72
66
|
|
|
73
|
-
def
|
|
74
|
-
|
|
67
|
+
def active_record50?
|
|
68
|
+
active_record5? && ::ActiveRecord::VERSION::MINOR == 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def active_record51?
|
|
72
|
+
active_record5? && ::ActiveRecord::VERSION::MINOR == 1
|
|
75
73
|
end
|
|
76
74
|
|
|
77
75
|
def mongoid4x?
|
|
78
76
|
mongoid? && ::Mongoid::VERSION =~ /\A4/
|
|
79
77
|
end
|
|
78
|
+
|
|
79
|
+
def mongoid5x?
|
|
80
|
+
mongoid? && ::Mongoid::VERSION =~ /\A5/
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def mongoid6x?
|
|
84
|
+
mongoid? && ::Mongoid::VERSION =~ /\A6/
|
|
85
|
+
end
|
|
80
86
|
end
|
|
81
87
|
end
|
|
@@ -20,6 +20,22 @@ module Bullet
|
|
|
20
20
|
call_object_associations.add(object.bullet_key, associations)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# possible_objects keep the class to object relationships
|
|
24
|
+
# that the objects may cause N+1 query.
|
|
25
|
+
# e.g. { Post => ["Post:1", "Post:2"] }
|
|
26
|
+
def possible_objects
|
|
27
|
+
Thread.current[:bullet_possible_objects]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# impossible_objects keep the class to objects relationships
|
|
31
|
+
# that the objects may not cause N+1 query.
|
|
32
|
+
# e.g. { Post => ["Post:1", "Post:2"] }
|
|
33
|
+
# if find collection returns only one object, then the object is impossible object,
|
|
34
|
+
# impossible_objects are used to avoid treating 1+1 query to N+1 query.
|
|
35
|
+
def impossible_objects
|
|
36
|
+
Thread.current[:bullet_impossible_objects]
|
|
37
|
+
end
|
|
38
|
+
|
|
23
39
|
private
|
|
24
40
|
# object_associations keep the object relationships
|
|
25
41
|
# that the object has many associations.
|
|
@@ -38,22 +54,6 @@ module Bullet
|
|
|
38
54
|
Thread.current[:bullet_call_object_associations]
|
|
39
55
|
end
|
|
40
56
|
|
|
41
|
-
# possible_objects keep the class to object relationships
|
|
42
|
-
# that the objects may cause N+1 query.
|
|
43
|
-
# e.g. { Post => ["Post:1", "Post:2"] }
|
|
44
|
-
def possible_objects
|
|
45
|
-
Thread.current[:bullet_possible_objects]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# impossible_objects keep the class to objects relationships
|
|
49
|
-
# that the objects may not cause N+1 query.
|
|
50
|
-
# e.g. { Post => ["Post:1", "Post:2"] }
|
|
51
|
-
# if find collection returns only one object, then the object is impossible object,
|
|
52
|
-
# impossible_objects are used to avoid treating 1+1 query to N+1 query.
|
|
53
|
-
def impossible_objects
|
|
54
|
-
Thread.current[:bullet_impossible_objects]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
57
|
# inversed_objects keeps object relationships
|
|
58
58
|
# that association is inversed.
|
|
59
59
|
# e.g. { "Comment:1" => ["post"] }
|
|
@@ -8,7 +8,7 @@ module Bullet
|
|
|
8
8
|
return unless object.primary_key_value
|
|
9
9
|
|
|
10
10
|
Bullet.debug("Detector::CounterCache#add_counter_cache", "object: #{object.bullet_key}, associations: #{associations}")
|
|
11
|
-
if conditions_met?(object
|
|
11
|
+
if conditions_met?(object, associations)
|
|
12
12
|
create_notification object.class.to_s, associations
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -32,6 +32,18 @@ module Bullet
|
|
|
32
32
|
impossible_objects.add object.bullet_key
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def conditions_met?(object, associations)
|
|
36
|
+
possible_objects.include?(object.bullet_key) && !impossible_objects.include?(object.bullet_key)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def possible_objects
|
|
40
|
+
Thread.current[:bullet_counter_possible_objects]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def impossible_objects
|
|
44
|
+
Thread.current[:bullet_counter_impossible_objects]
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
private
|
|
36
48
|
def create_notification(klazz, associations)
|
|
37
49
|
notify_associations = Array(associations) - Bullet.get_whitelist_associations(:counter_cache, klazz)
|
|
@@ -41,18 +53,6 @@ module Bullet
|
|
|
41
53
|
Bullet.notification_collector.add notice
|
|
42
54
|
end
|
|
43
55
|
end
|
|
44
|
-
|
|
45
|
-
def possible_objects
|
|
46
|
-
Thread.current[:bullet_counter_possible_objects]
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def impossible_objects
|
|
50
|
-
Thread.current[:bullet_counter_impossible_objects]
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def conditions_met?(bullet_key, associations)
|
|
54
|
-
possible_objects.include?(bullet_key) && !impossible_objects.include?(bullet_key)
|
|
55
|
-
end
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -2,6 +2,7 @@ module Bullet
|
|
|
2
2
|
module Detector
|
|
3
3
|
class NPlusOneQuery < Association
|
|
4
4
|
extend Dependency
|
|
5
|
+
extend StackTraceFilter
|
|
5
6
|
|
|
6
7
|
class <<self
|
|
7
8
|
# executed when object.assocations is called.
|
|
@@ -15,7 +16,7 @@ module Bullet
|
|
|
15
16
|
add_call_object_associations(object, associations)
|
|
16
17
|
|
|
17
18
|
Bullet.debug("Detector::NPlusOneQuery#call_association", "object: #{object.bullet_key}, associations: #{associations}")
|
|
18
|
-
if conditions_met?(object
|
|
19
|
+
if !excluded_stacktrace_path? && conditions_met?(object, associations)
|
|
19
20
|
Bullet.debug("detect n + 1 query", "object: #{object.bullet_key}, associations: #{associations}")
|
|
20
21
|
create_notification caller_in_project, object.class.to_s, associations
|
|
21
22
|
end
|
|
@@ -49,49 +50,43 @@ module Bullet
|
|
|
49
50
|
inversed_objects.add object.bullet_key, association
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
# decide whether the object.associations is unpreloaded or not.
|
|
54
|
+
def conditions_met?(object, associations)
|
|
55
|
+
possible?(object) && !impossible?(object) && !association?(object, associations)
|
|
56
|
+
end
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
end
|
|
60
|
-
end
|
|
58
|
+
def possible?(object)
|
|
59
|
+
possible_objects.include? object.bullet_key
|
|
60
|
+
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
62
|
+
def impossible?(object)
|
|
63
|
+
impossible_objects.include? object.bullet_key
|
|
64
|
+
end
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
# check if object => associations already exists in object_associations.
|
|
67
|
+
def association?(object, associations)
|
|
68
|
+
value = object_associations[object.bullet_key]
|
|
69
|
+
if value
|
|
70
|
+
value.each do |v|
|
|
71
|
+
# associations == v comparision order is important here because
|
|
72
|
+
# v variable might be a squeel node where :== method is redefined,
|
|
73
|
+
# so it does not compare values at all and return unexpected results
|
|
74
|
+
result = v.is_a?(Hash) ? v.key?(associations) : associations == v
|
|
75
|
+
return true if result
|
|
73
76
|
end
|
|
74
77
|
end
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
79
|
+
false
|
|
80
|
+
end
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
private
|
|
83
|
+
def create_notification(callers, klazz, associations)
|
|
84
|
+
notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if value
|
|
88
|
-
value.each do |v|
|
|
89
|
-
result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
|
|
90
|
-
return true if result
|
|
91
|
-
end
|
|
86
|
+
if notify_associations.present?
|
|
87
|
+
notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
|
|
88
|
+
Bullet.notification_collector.add(notice)
|
|
92
89
|
end
|
|
93
|
-
|
|
94
|
-
return false
|
|
95
90
|
end
|
|
96
91
|
end
|
|
97
92
|
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module Bullet
|
|
2
2
|
module Detector
|
|
3
3
|
class UnusedEagerLoading < Association
|
|
4
|
+
extend Dependency
|
|
5
|
+
extend StackTraceFilter
|
|
6
|
+
|
|
4
7
|
class <<self
|
|
5
8
|
# check if there are unused preload associations.
|
|
6
9
|
# get related_objects from eager_loadings associated with object and associations
|
|
@@ -15,7 +18,7 @@ module Bullet
|
|
|
15
18
|
next if object_association_diff.empty?
|
|
16
19
|
|
|
17
20
|
Bullet.debug("detect unused preload", "object: #{bullet_key}, associations: #{object_association_diff}")
|
|
18
|
-
create_notification bullet_key.bullet_class_name, object_association_diff
|
|
21
|
+
create_notification(caller_in_project, bullet_key.bullet_class_name, object_association_diff)
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
|
|
@@ -27,39 +30,37 @@ module Bullet
|
|
|
27
30
|
Bullet.debug("Detector::UnusedEagerLoading#add_eager_loadings", "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}")
|
|
28
31
|
bullet_keys = objects.map(&:bullet_key)
|
|
29
32
|
|
|
30
|
-
to_add =
|
|
31
|
-
to_merge, to_delete = [], []
|
|
33
|
+
to_add, to_merge, to_delete = [], [], []
|
|
32
34
|
eager_loadings.each do |k, v|
|
|
33
35
|
key_objects_overlap = k & bullet_keys
|
|
34
36
|
|
|
35
37
|
next if key_objects_overlap.empty?
|
|
36
38
|
|
|
39
|
+
bullet_keys = bullet_keys - k
|
|
37
40
|
if key_objects_overlap == k
|
|
38
|
-
to_add
|
|
39
|
-
break
|
|
41
|
+
to_add << [k, associations]
|
|
40
42
|
else
|
|
41
43
|
to_merge << [key_objects_overlap, ( eager_loadings[k].dup << associations )]
|
|
42
44
|
|
|
43
|
-
keys_without_objects = k -
|
|
45
|
+
keys_without_objects = k - key_objects_overlap
|
|
44
46
|
to_merge << [keys_without_objects, eager_loadings[k]]
|
|
45
47
|
to_delete << k
|
|
46
|
-
bullet_keys = bullet_keys - k
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
eager_loadings.add
|
|
51
|
-
to_merge.each { |k,val| eager_loadings.merge k, val }
|
|
51
|
+
to_add.each { |k, val| eager_loadings.add k, val }
|
|
52
|
+
to_merge.each { |k, val| eager_loadings.merge k, val }
|
|
52
53
|
to_delete.each { |k| eager_loadings.delete k }
|
|
53
54
|
|
|
54
55
|
eager_loadings.add bullet_keys, associations unless bullet_keys.empty?
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
private
|
|
58
|
-
def create_notification(klazz, associations)
|
|
59
|
+
def create_notification(callers, klazz, associations)
|
|
59
60
|
notify_associations = Array(associations) - Bullet.get_whitelist_associations(:unused_eager_loading, klazz)
|
|
60
61
|
|
|
61
62
|
if notify_associations.present?
|
|
62
|
-
notice = Bullet::Notification::UnusedEagerLoading.new(klazz, notify_associations)
|
|
63
|
+
notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations)
|
|
63
64
|
Bullet.notification_collector.add(notice)
|
|
64
65
|
end
|
|
65
66
|
end
|
data/lib/bullet/ext/object.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
class Object
|
|
2
2
|
def bullet_key
|
|
3
|
-
|
|
3
|
+
"#{self.class}:#{self.primary_key_value}"
|
|
4
4
|
end
|
|
5
5
|
|
|
6
6
|
def primary_key_value
|
|
7
|
-
if self.class.respond_to?(:
|
|
7
|
+
if self.class.respond_to?(:primary_keys) && self.class.primary_keys
|
|
8
|
+
self.class.primary_keys.map { |primary_key| self.send primary_key }.join(',')
|
|
9
|
+
elsif self.class.respond_to?(:primary_key) && self.class.primary_key
|
|
8
10
|
self.send self.class.primary_key
|
|
9
11
|
else
|
|
10
12
|
self.id
|
|
@@ -21,7 +21,7 @@ module Bullet
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def each(&block)
|
|
24
|
-
records =
|
|
24
|
+
records = view.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) }
|
|
25
25
|
if records.length > 1
|
|
26
26
|
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
|
|
27
27
|
elsif records.size == 1
|
|
@@ -43,8 +43,8 @@ module Bullet
|
|
|
43
43
|
::Mongoid::Relations::Accessors.class_eval do
|
|
44
44
|
alias_method :origin_get_relation, :get_relation
|
|
45
45
|
|
|
46
|
-
def get_relation(name, metadata, reload = false)
|
|
47
|
-
result = origin_get_relation(name, metadata, reload)
|
|
46
|
+
def get_relation(name, metadata, object, reload = false)
|
|
47
|
+
result = origin_get_relation(name, metadata, object, reload)
|
|
48
48
|
if metadata.macro !~ /embed/
|
|
49
49
|
Bullet::Detector::NPlusOneQuery.call_association(self, name)
|
|
50
50
|
end
|