module Bullet class BulletAssociationError < StandardError end class Association class < #{associations.inspect}" unpreload_associations[klazz] ||= [] unpreload_associations[klazz] << associations unique(unpreload_associations[klazz]) end def add_unused_preload_associations(klazz, associations) # puts "add unused preload associations, #{klazz} => #{associations.inspect}" unused_preload_associations[klazz] ||= [] unused_preload_associations[klazz] << associations unique(unused_preload_associations[klazz]) end def add_association(object, associations) # puts "add associations, #{object} => #{associations.inspect}" object_associations[object] ||= [] object_associations[object] << associations unique(object_associations[object]) end ################################################### # interface for active record ################################################### def add_call_object_associations(object, associations) # puts "add call object associations, #{object} => #{associations.inspect}" call_object_associations[object] ||= [] call_object_associations[object] << associations unique(call_object_associations[object]) end def add_possible_objects(objects) # puts "add possible objects, #{objects.inspect}" klazz= objects.first.class possible_objects[klazz] ||= [] possible_objects[klazz] << objects unique(possible_objects[klazz]) end def add_impossible_object(object) # puts "add impossible object, #{object}" klazz = object.class impossible_objects[klazz] ||= [] impossible_objects[klazz] << object impossible_objects[klazz].uniq! end def add_klazz_associations(klazz, associations) # puts "define associations, #{klazz} => #{associations.inspect}" klazz_associations[klazz] ||= [] klazz_associations[klazz] << associations unique(klazz_associations[klazz]) end def add_eager_loadings(objects, associations) # puts "add eager loadings, #{objects.inspect} => #{associations.inspect}" objects = Array(objects) eager_loadings[objects] ||= [] eager_loadings[objects] << associations unique(eager_loadings[objects]) end def define_association(klazz, associations) # puts "define association, #{klazz} => #{associations.inspect}" add_klazz_associations(klazz, associations) end def call_association(object, associations) # puts "call association, #{object} => #{associations.inspect}" add_call_object_associations(object, associations) if unpreload_associations?(object, associations) add_unpreload_associations(object.class, associations) caller_in_project end end ############################################ # for rspec ############################################ def check_unused_preload_associations object_associations.each do |object, association| related_objects = eager_loadings.select {|key, value| key.include?(object) and value == association}.collect(&:first).flatten call_object_association = related_objects.collect { |related_object| call_object_associations[related_object] }.compact.flatten.uniq diff_object_association = (association - call_object_association).reject {|a| a.is_a? Hash} add_unused_preload_associations(object.class, diff_object_association) unless diff_object_association.empty? end end def has_unused_preload_associations? !unused_preload_associations.empty? end def has_unpreload_associations? !unpreload_associations.empty? end private def unpreload_associations?(object, associations) klazz = object.class (!possible_objects[klazz].nil? and possible_objects[klazz].include?(object)) and (impossible_objects[klazz].nil? or !impossible_objects[klazz].include?(object)) and (object_associations[object].nil? or !object_associations[object].include?(associations)) end def notification_response response = [] if has_unused_preload_associations? response << unused_preload_messages.join("\n") end if has_unpreload_associations? response << unpreload_messages.join("\n") end response end def unused_preload_messages(path = nil) messages = [] unused_preload_associations.each do |klazz, associations| messages << [ "Unused Eager Loading #{path ? "in #{path}" : 'detected'}", klazz_associations_str(klazz, associations), " Remove from your finder: #{associations_str(associations)}" ] end messages end def unpreload_messages(path = nil) messages = [] unpreload_associations.each do |klazz, associations| messages << [ "N+1 Query #{path ? "in #{path}" : 'detected'}", klazz_associations_str(klazz, associations), " Add to your finder: #{associations_str(associations)}" ] end messages end def klazz_associations_str(klazz, associations) " #{klazz} => [#{associations.map(&:inspect).join(', ')}]" end def associations_str(associations) ":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}" end def wrap_js_association(message) str = '' str << "\n" end def call_stack_messages callers.inject([]) do |messages, c| messages << ['N+1 Query method call stack', c.collect {|line| " #{line}"}].flatten end end def unique(array) array.flatten! array.uniq! end def unpreload_associations @@unpreload_associations ||= {} end def unused_preload_associations @@unused_preload_associations ||= {} end def object_associations @@object_associations ||= {} end def call_object_associations @@call_object_associations ||= {} end def possible_objects @@possible_objects ||= {} end def impossible_objects @@impossible_objects ||= {} end def klazz_associations @@klazz_associations ||= {} end def eager_loadings @@eager_loadings ||= {} end VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor') def caller_in_project callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/} callers.uniq! end def callers @@callers ||= [] end end end end