bullet 5.7.5 → 6.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +49 -12
  4. data/Gemfile.mongoid-7.0 +15 -0
  5. data/Gemfile.rails-4.0 +1 -1
  6. data/Gemfile.rails-4.1 +1 -1
  7. data/Gemfile.rails-4.2 +1 -1
  8. data/Gemfile.rails-5.0 +1 -1
  9. data/Gemfile.rails-5.1 +1 -1
  10. data/Gemfile.rails-5.2 +2 -2
  11. data/Gemfile.rails-6.0 +15 -0
  12. data/Gemfile.rails-6.1 +15 -0
  13. data/README.md +38 -13
  14. data/Rakefile +1 -1
  15. data/bullet.gemspec +8 -3
  16. data/lib/bullet.rb +50 -22
  17. data/lib/bullet/active_job.rb +13 -0
  18. data/lib/bullet/active_record4.rb +12 -35
  19. data/lib/bullet/active_record41.rb +10 -30
  20. data/lib/bullet/active_record42.rb +12 -27
  21. data/lib/bullet/active_record5.rb +197 -177
  22. data/lib/bullet/active_record52.rb +191 -166
  23. data/lib/bullet/active_record60.rb +278 -0
  24. data/lib/bullet/active_record61.rb +278 -0
  25. data/lib/bullet/bullet_xhr.js +63 -0
  26. data/lib/bullet/dependency.rb +54 -34
  27. data/lib/bullet/detector/association.rb +26 -20
  28. data/lib/bullet/detector/base.rb +1 -2
  29. data/lib/bullet/detector/counter_cache.rb +14 -9
  30. data/lib/bullet/detector/n_plus_one_query.rb +27 -17
  31. data/lib/bullet/detector/unused_eager_loading.rb +7 -3
  32. data/lib/bullet/ext/object.rb +5 -3
  33. data/lib/bullet/ext/string.rb +1 -1
  34. data/lib/bullet/mongoid4x.rb +4 -7
  35. data/lib/bullet/mongoid5x.rb +4 -7
  36. data/lib/bullet/mongoid6x.rb +8 -11
  37. data/lib/bullet/mongoid7x.rb +57 -0
  38. data/lib/bullet/notification/base.rb +15 -19
  39. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  40. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  41. data/lib/bullet/rack.rb +54 -28
  42. data/lib/bullet/stack_trace_filter.rb +39 -30
  43. data/lib/bullet/version.rb +1 -1
  44. data/lib/generators/bullet/install_generator.rb +26 -26
  45. data/perf/benchmark.rb +8 -14
  46. data/spec/bullet/detector/counter_cache_spec.rb +6 -6
  47. data/spec/bullet/detector/n_plus_one_query_spec.rb +30 -3
  48. data/spec/bullet/detector/unused_eager_loading_spec.rb +19 -6
  49. data/spec/bullet/ext/object_spec.rb +9 -4
  50. data/spec/bullet/notification/base_spec.rb +1 -3
  51. data/spec/bullet/notification/n_plus_one_query_spec.rb +16 -3
  52. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  53. data/spec/bullet/rack_spec.rb +140 -5
  54. data/spec/bullet/registry/association_spec.rb +2 -2
  55. data/spec/bullet/registry/base_spec.rb +1 -1
  56. data/spec/bullet_spec.rb +10 -29
  57. data/spec/integration/active_record/association_spec.rb +122 -118
  58. data/spec/integration/counter_cache_spec.rb +11 -31
  59. data/spec/integration/mongoid/association_spec.rb +18 -32
  60. data/spec/models/attachment.rb +5 -0
  61. data/spec/models/client.rb +2 -0
  62. data/spec/models/firm.rb +1 -0
  63. data/spec/models/folder.rb +1 -2
  64. data/spec/models/group.rb +3 -0
  65. data/spec/models/page.rb +1 -2
  66. data/spec/models/post.rb +15 -0
  67. data/spec/models/submission.rb +1 -0
  68. data/spec/models/user.rb +1 -0
  69. data/spec/models/writer.rb +1 -2
  70. data/spec/spec_helper.rb +6 -10
  71. data/spec/support/bullet_ext.rb +8 -9
  72. data/spec/support/mongo_seed.rb +2 -16
  73. data/spec/support/sqlite_seed.rb +17 -2
  74. data/test.sh +2 -0
  75. data/update.sh +1 -0
  76. metadata +24 -11
@@ -3,22 +3,28 @@
3
3
  module Bullet
4
4
  module Detector
5
5
  class Association < Base
6
- class <<self
6
+ class << self
7
7
  def add_object_associations(object, associations)
8
8
  return unless Bullet.start?
9
9
  return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable?
10
- return unless object.primary_key_value
10
+ return unless object.bullet_primary_key_value
11
11
 
12
- Bullet.debug('Detector::Association#add_object_associations'.freeze, "object: #{object.bullet_key}, associations: #{associations}")
12
+ Bullet.debug(
13
+ 'Detector::Association#add_object_associations',
14
+ "object: #{object.bullet_key}, associations: #{associations}"
15
+ )
13
16
  object_associations.add(object.bullet_key, associations)
14
17
  end
15
18
 
16
19
  def add_call_object_associations(object, associations)
17
20
  return unless Bullet.start?
18
21
  return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable?
19
- return unless object.primary_key_value
22
+ return unless object.bullet_primary_key_value
20
23
 
21
- Bullet.debug('Detector::Association#add_call_object_associations'.freeze, "object: #{object.bullet_key}, associations: #{associations}")
24
+ Bullet.debug(
25
+ 'Detector::Association#add_call_object_associations',
26
+ "object: #{object.bullet_key}, associations: #{associations}"
27
+ )
22
28
  call_object_associations.add(object.bullet_key, associations)
23
29
  end
24
30
 
@@ -40,33 +46,33 @@ module Bullet
40
46
 
41
47
  private
42
48
 
43
- # object_associations keep the object relationships
44
- # that the object has many associations.
45
- # e.g. { "Post:1" => [:comments] }
46
- # the object_associations keep all associations that may be or may no be
47
- # unpreload associations or unused preload associations.
49
+ # object_associations keep the object relationships
50
+ # that the object has many associations.
51
+ # e.g. { "Post:1" => [:comments] }
52
+ # the object_associations keep all associations that may be or may no be
53
+ # unpreload associations or unused preload associations.
48
54
  def object_associations
49
55
  Thread.current[:bullet_object_associations]
50
56
  end
51
57
 
52
- # call_object_associations keep the object relationships
53
- # that object.associations is called.
54
- # e.g. { "Post:1" => [:comments] }
55
- # they are used to detect unused preload associations.
58
+ # call_object_associations keep the object relationships
59
+ # that object.associations is called.
60
+ # e.g. { "Post:1" => [:comments] }
61
+ # they are used to detect unused preload associations.
56
62
  def call_object_associations
57
63
  Thread.current[:bullet_call_object_associations]
58
64
  end
59
65
 
60
- # inversed_objects keeps object relationships
61
- # that association is inversed.
62
- # e.g. { "Comment:1" => ["post"] }
66
+ # inversed_objects keeps object relationships
67
+ # that association is inversed.
68
+ # e.g. { "Comment:1" => ["post"] }
63
69
  def inversed_objects
64
70
  Thread.current[:bullet_inversed_objects]
65
71
  end
66
72
 
67
- # eager_loadings keep the object relationships
68
- # that the associations are preloaded by find :include.
69
- # e.g. { ["Post:1", "Post:2"] => [:comments, :user] }
73
+ # eager_loadings keep the object relationships
74
+ # that the associations are preloaded by find :include.
75
+ # e.g. { ["Post:1", "Post:2"] => [:comments, :user] }
70
76
  def eager_loadings
71
77
  Thread.current[:bullet_eager_loadings]
72
78
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Bullet
4
4
  module Detector
5
- class Base
6
- end
5
+ class Base; end
7
6
  end
8
7
  end
@@ -3,32 +3,37 @@
3
3
  module Bullet
4
4
  module Detector
5
5
  class CounterCache < Base
6
- class <<self
6
+ class << self
7
7
  def add_counter_cache(object, associations)
8
8
  return unless Bullet.start?
9
9
  return unless Bullet.counter_cache_enable?
10
- return unless object.primary_key_value
10
+ return unless object.bullet_primary_key_value
11
11
 
12
- Bullet.debug('Detector::CounterCache#add_counter_cache', "object: #{object.bullet_key}, associations: #{associations}")
13
- if conditions_met?(object, associations)
14
- create_notification object.class.to_s, associations
15
- end
12
+ Bullet.debug(
13
+ 'Detector::CounterCache#add_counter_cache',
14
+ "object: #{object.bullet_key}, associations: #{associations}"
15
+ )
16
+ create_notification object.class.to_s, associations if conditions_met?(object, associations)
16
17
  end
17
18
 
18
19
  def add_possible_objects(object_or_objects)
19
20
  return unless Bullet.start?
20
21
  return unless Bullet.counter_cache_enable?
22
+
21
23
  objects = Array(object_or_objects)
22
- return if objects.map(&:primary_key_value).compact.empty?
24
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
23
25
 
24
- Bullet.debug('Detector::CounterCache#add_possible_objects', "objects: #{objects.map(&:bullet_key).join(', ')}")
26
+ Bullet.debug(
27
+ 'Detector::CounterCache#add_possible_objects',
28
+ "objects: #{objects.map(&:bullet_key).join(', ')}"
29
+ )
25
30
  objects.each { |object| possible_objects.add object.bullet_key }
26
31
  end
27
32
 
28
33
  def add_impossible_object(object)
29
34
  return unless Bullet.start?
30
35
  return unless Bullet.counter_cache_enable?
31
- return unless object.primary_key_value
36
+ return unless object.bullet_primary_key_value
32
37
 
33
38
  Bullet.debug('Detector::CounterCache#add_impossible_object', "object: #{object.bullet_key}")
34
39
  impossible_objects.add object.bullet_key
@@ -6,7 +6,7 @@ module Bullet
6
6
  extend Dependency
7
7
  extend StackTraceFilter
8
8
 
9
- class <<self
9
+ class << self
10
10
  # executed when object.assocations is called.
11
11
  # first, it keeps this method call for object.association.
12
12
  # then, it checks if this associations call is unpreload.
@@ -14,11 +14,15 @@ module Bullet
14
14
  def call_association(object, associations)
15
15
  return unless Bullet.start?
16
16
  return unless Bullet.n_plus_one_query_enable?
17
- return unless object.primary_key_value
17
+ return unless object.bullet_primary_key_value
18
18
  return if inversed_objects.include?(object.bullet_key, associations)
19
+
19
20
  add_call_object_associations(object, associations)
20
21
 
21
- Bullet.debug('Detector::NPlusOneQuery#call_association'.freeze, "object: #{object.bullet_key}, associations: #{associations}")
22
+ Bullet.debug(
23
+ 'Detector::NPlusOneQuery#call_association',
24
+ "object: #{object.bullet_key}, associations: #{associations}"
25
+ )
22
26
  if !excluded_stacktrace_path? && conditions_met?(object, associations)
23
27
  Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
24
28
  create_notification caller_in_project, object.class.to_s, associations
@@ -28,28 +32,35 @@ module Bullet
28
32
  def add_possible_objects(object_or_objects)
29
33
  return unless Bullet.start?
30
34
  return unless Bullet.n_plus_one_query_enable?
35
+
31
36
  objects = Array(object_or_objects)
32
- return if objects.map(&:primary_key_value).compact.empty?
37
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
33
38
 
34
- Bullet.debug('Detector::NPlusOneQuery#add_possible_objects'.freeze, "objects: #{objects.map(&:bullet_key).join(', '.freeze)}")
39
+ Bullet.debug(
40
+ 'Detector::NPlusOneQuery#add_possible_objects',
41
+ "objects: #{objects.map(&:bullet_key).join(', ')}"
42
+ )
35
43
  objects.each { |object| possible_objects.add object.bullet_key }
36
44
  end
37
45
 
38
46
  def add_impossible_object(object)
39
47
  return unless Bullet.start?
40
48
  return unless Bullet.n_plus_one_query_enable?
41
- return unless object.primary_key_value
49
+ return unless object.bullet_primary_key_value
42
50
 
43
- Bullet.debug('Detector::NPlusOneQuery#add_impossible_object'.freeze, "object: #{object.bullet_key}")
51
+ Bullet.debug('Detector::NPlusOneQuery#add_impossible_object', "object: #{object.bullet_key}")
44
52
  impossible_objects.add object.bullet_key
45
53
  end
46
54
 
47
55
  def add_inversed_object(object, association)
48
56
  return unless Bullet.start?
49
57
  return unless Bullet.n_plus_one_query_enable?
50
- return unless object.primary_key_value
58
+ return unless object.bullet_primary_key_value
51
59
 
52
- Bullet.debug('Detector::NPlusOneQuery#add_inversed_object'.freeze, "object: #{object.bullet_key}, association: #{association}")
60
+ Bullet.debug(
61
+ 'Detector::NPlusOneQuery#add_inversed_object',
62
+ "object: #{object.bullet_key}, association: #{association}"
63
+ )
53
64
  inversed_objects.add object.bullet_key, association
54
65
  end
55
66
 
@@ -69,14 +80,13 @@ module Bullet
69
80
  # check if object => associations already exists in object_associations.
70
81
  def association?(object, associations)
71
82
  value = object_associations[object.bullet_key]
72
- if value
73
- value.each do |v|
74
- # associations == v comparison order is important here because
75
- # v variable might be a squeel node where :== method is redefined,
76
- # so it does not compare values at all and return unexpected results
77
- result = v.is_a?(Hash) ? v.key?(associations) : associations == v
78
- return true if result
79
- end
83
+ value&.each do |v|
84
+ # associations == v comparison order is important here because
85
+ # v variable might be a squeel node where :== method is redefined,
86
+ # so it does not compare values at all and return unexpected results
87
+ result =
88
+ v.is_a?(Hash) ? v.key?(associations) : associations == v
89
+ return true if result
80
90
  end
81
91
 
82
92
  false
@@ -6,7 +6,7 @@ module Bullet
6
6
  extend Dependency
7
7
  extend StackTraceFilter
8
8
 
9
- class <<self
9
+ class << self
10
10
  # check if there are unused preload associations.
11
11
  # get related_objects from eager_loadings associated with object and associations
12
12
  # get call_object_association from associations of call_object_associations whose object is in related_objects
@@ -27,9 +27,12 @@ module Bullet
27
27
  def add_eager_loadings(objects, associations)
28
28
  return unless Bullet.start?
29
29
  return unless Bullet.unused_eager_loading_enable?
30
- return if objects.map(&:primary_key_value).compact.empty?
30
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
31
31
 
32
- Bullet.debug('Detector::UnusedEagerLoading#add_eager_loadings', "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}")
32
+ Bullet.debug(
33
+ 'Detector::UnusedEagerLoading#add_eager_loadings',
34
+ "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}"
35
+ )
33
36
  bullet_keys = objects.map(&:bullet_key)
34
37
 
35
38
  to_add = []
@@ -75,6 +78,7 @@ module Bullet
75
78
  eager_loadings.similarly_associated(bullet_key, associations).each do |related_bullet_key|
76
79
  coa = call_object_associations[related_bullet_key]
77
80
  next if coa.nil?
81
+
78
82
  all.merge coa
79
83
  end
80
84
  all.to_a
@@ -2,12 +2,14 @@
2
2
 
3
3
  class Object
4
4
  def bullet_key
5
- "#{self.class}:#{primary_key_value}"
5
+ "#{self.class}:#{bullet_primary_key_value}"
6
6
  end
7
7
 
8
- def primary_key_value
8
+ def bullet_primary_key_value
9
+ return if respond_to?(:persisted?) && !persisted?
10
+
9
11
  if self.class.respond_to?(:primary_keys) && self.class.primary_keys
10
- self.class.primary_keys.map { |primary_key| send primary_key }.join(','.freeze)
12
+ self.class.primary_keys.map { |primary_key| send primary_key }.join(',')
11
13
  elsif self.class.respond_to?(:primary_key) && self.class.primary_key
12
14
  send self.class.primary_key
13
15
  else
@@ -2,6 +2,6 @@
2
2
 
3
3
  class String
4
4
  def bullet_class_name
5
- sub(/:[^:]*?$/, ''.freeze)
5
+ sub(/:[^:]*?$/, '')
6
6
  end
7
7
  end
@@ -23,7 +23,8 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
+
27
28
  records = []
28
29
  origin_each { |record| records << record }
29
30
  if records.length > 1
@@ -36,9 +37,7 @@ module Bullet
36
37
 
37
38
  def eager_load(docs)
38
39
  associations = criteria.inclusions.map(&:name)
39
- docs.each do |doc|
40
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
41
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
42
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
43
42
  origin_eager_load(docs)
44
43
  end
@@ -49,9 +48,7 @@ module Bullet
49
48
 
50
49
  def get_relation(name, metadata, object, reload = false)
51
50
  result = origin_get_relation(name, metadata, object, reload)
52
- if metadata.macro !~ /embed/
53
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
54
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
55
52
  result
56
53
  end
57
54
  end
@@ -23,7 +23,8 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
+
27
28
  records = []
28
29
  origin_each { |record| records << record }
29
30
  if records.length > 1
@@ -36,9 +37,7 @@ module Bullet
36
37
 
37
38
  def eager_load(docs)
38
39
  associations = criteria.inclusions.map(&:name)
39
- docs.each do |doc|
40
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
41
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
42
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
43
42
  origin_eager_load(docs)
44
43
  end
@@ -49,9 +48,7 @@ module Bullet
49
48
 
50
49
  def get_relation(name, metadata, object, reload = false)
51
50
  result = origin_get_relation(name, metadata, object, reload)
52
- if metadata.macro !~ /embed/
53
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
54
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
55
52
  result
56
53
  end
57
54
  end
@@ -10,20 +10,21 @@ module Bullet
10
10
  alias_method :origin_each, :each
11
11
  alias_method :origin_eager_load, :eager_load
12
12
 
13
- def first
14
- result = origin_first
13
+ def first(opt = {})
14
+ result = origin_first(opt)
15
15
  Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
16
  result
17
17
  end
18
18
 
19
- def last
20
- result = origin_last
19
+ def last(opt = {})
20
+ result = origin_last(opt)
21
21
  Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
22
  result
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
+
27
28
  records = []
28
29
  origin_each { |record| records << record }
29
30
  if records.length > 1
@@ -36,9 +37,7 @@ module Bullet
36
37
 
37
38
  def eager_load(docs)
38
39
  associations = criteria.inclusions.map(&:name)
39
- docs.each do |doc|
40
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
41
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
42
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
43
42
  origin_eager_load(docs)
44
43
  end
@@ -49,9 +48,7 @@ module Bullet
49
48
 
50
49
  def get_relation(name, metadata, object, reload = false)
51
50
  result = origin_get_relation(name, metadata, object, reload)
52
- if metadata.macro !~ /embed/
53
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
54
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
55
52
  result
56
53
  end
57
54
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module Mongoid
5
+ def self.enable
6
+ require 'mongoid'
7
+ ::Mongoid::Contextual::Mongo.class_eval do
8
+ alias_method :origin_first, :first
9
+ alias_method :origin_last, :last
10
+ alias_method :origin_each, :each
11
+ alias_method :origin_eager_load, :eager_load
12
+
13
+ def first(opts = {})
14
+ result = origin_first(opts)
15
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
+ result
17
+ end
18
+
19
+ def last(opts = {})
20
+ result = origin_last(opts)
21
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
+ result
23
+ end
24
+
25
+ def each(&block)
26
+ return to_enum unless block
27
+
28
+ records = []
29
+ origin_each { |record| records << record }
30
+ if records.length > 1
31
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
32
+ elsif records.size == 1
33
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
34
+ end
35
+ records.each(&block)
36
+ end
37
+
38
+ def eager_load(docs)
39
+ associations = criteria.inclusions.map(&:name)
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
41
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
42
+ origin_eager_load(docs)
43
+ end
44
+ end
45
+
46
+ ::Mongoid::Association::Accessors.class_eval do
47
+ alias_method :origin_get_relation, :get_relation
48
+
49
+ def get_relation(name, association, object, reload = false)
50
+ result = origin_get_relation(name, association, object, reload)
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
52
+ result
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end