48 votes

Compter le nombre de requêtes effectuées

J'aimerais tester qu'un certain morceau de code exécute le moins de requêtes SQL possible.

ActiveRecord::TestCase semble avoir sa propre assert_queries qui fera exactement cela. Mais comme je ne suis pas Parcheando ActiveRecord, cela ne me sert pas à grand chose.

RSpec ou ActiveRecord fournissent-ils un moyen officiel et public de compter le nombre de requêtes SQL exécutées dans un bloc de code ?

55voto

Ryan Bigg Points 64561

Je pense que vous avez répondu à votre propre question en mentionnant assert_queries mais c'est parti :

Je vous recommande de jeter un coup d'œil au code qui se trouve derrière assert_queries et l'utiliser pour construire votre propre méthode que vous pouvez utiliser pour compter les requêtes. La principale magie impliquée ici est cette ligne :

ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)

J'ai un peu bricolé ce matin et j'ai retiré les parties d'ActiveRecord qui font le comptage des requêtes et j'ai trouvé ceci :

module ActiveRecord
  class QueryCounter
    cattr_accessor :query_count do
      0
    end

    IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/]

    def call(name, start, finish, message_id, values)
      # FIXME: this seems bad. we should probably have a better way to indicate
      # the query was cached
      unless 'CACHE' == values[:name]
        self.class.query_count += 1 unless IGNORED_SQL.any? { |r| values[:sql] =~ r }
      end
    end
  end
end

ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::QueryCounter.new)

module ActiveRecord
  class Base
    def self.count_queries(&block)
      ActiveRecord::QueryCounter.query_count = 0
      yield
      ActiveRecord::QueryCounter.query_count
    end
  end
end

Vous pourrez faire référence à la ActiveRecord::Base.count_queries n'importe où. Passez-lui un bloc dans lequel vos requêtes sont exécutées et elle retournera le nombre de requêtes qui ont été exécutées :

ActiveRecord::Base.count_queries do
  Ticket.first
end

Il renvoie "1" pour moi. Pour que cela fonctionne : mettez-le dans un fichier à lib/active_record/query_counter.rb et l'exiger dans votre config/application.rb comme ceci :

require 'active_record/query_counter'

Hey presto !


Un peu d'explication est probablement nécessaire. Quand on appelle cette ligne :

    ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::QueryCounter.new)

Nous utilisons le petit cadre de notifications de Rails 3. C'est un petit ajout brillant à la dernière version majeure de Rails que personne ne connaît vraiment. Il nous permet de nous abonner à des notifications d'événements dans Rails en utilisant la fonction subscribe méthode. Nous transmettons l'événement auquel nous voulons nous abonner en tant que premier argument, puis tout objet qui répond à la méthode call comme le second.

Dans ce cas, lorsqu'une requête est exécutée, notre petit compteur de requêtes incrémente consciencieusement la variable ActiveRecord::QueryCounter.query_count, mais seulement pour la période de temps suivante. réel des requêtes.

Bref, c'était sympa. J'espère que ça vous sera utile.

2 votes

Excellent script. Si vous ne l'utilisez que pour les tests, vous pouvez le mettre dans un fichier {spec|test}/support/query_counter.rb. Gardez le dossier lib pour la logique de l'application.

1 votes

Pour ceux qui recherchent un matcheur RSpec, cette réponse a été transformée en un bijou : rspec-sqlimit .

24voto

Yuriy Kharchenko Points 421

Ma vision du script de Ryan (nettoyé un peu et enveloppé dans un matcher), j'espère qu'il est toujours d'actualité pour quelqu'un :

J'ai mis ceci dans spec/support/query_counter.rb

module ActiveRecord
  class QueryCounter

    attr_reader :query_count

    def initialize
      @query_count = 0
    end

    def to_proc
      lambda(&method(:callback))
    end

    def callback(name, start, finish, message_id, values)
      @query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name])
    end

  end
end

et ceci à spec/support/matchers/exceed_query_limit.rb

RSpec::Matchers.define :exceed_query_limit do |expected|

  match do |block|
    query_count(&block) > expected
  end

  failure_message_for_should_not do |actual|
    "Expected to run maximum #{expected} queries, got #{@counter.query_count}"
  end

  def query_count(&block)
    @counter = ActiveRecord::QueryCounter.new
    ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
    @counter.query_count
  end

end

Utilisation :

expect { MyModel.do_the_queries }.to_not exceed_query_limit(2)

4 votes

Mises à jour mineures pour RSpec 3 dans cette phrase .

13voto

Jaime Cham Points 510

Voici une autre formulation de la solution de Ryan et de Yuriy, qui est simplement une fonction que vous ajoutez à votre test_helper.rb :

def count_queries &block
  count = 0

  counter_f = ->(name, started, finished, unique_id, payload) {
    unless payload[:name].in? %w[ CACHE SCHEMA ]
      count += 1
    end
  }

  ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block)

  count
end

L'utilisation est juste :

c = count_queries do
  SomeModel.first
end

7voto

grosser Points 4300
  • message d'erreur utile
  • supprime les abonnés après l'exécution

(basé sur la réponse de Jaime Cham)

class ActiveSupport::TestCase
  def sql_queries(&block)
    queries = []
    counter = ->(*, payload) {
      queries << payload.fetch(:sql) unless ["CACHE", "SCHEMA"].include?(payload.fetch(:name))
    }

    ActiveSupport::Notifications.subscribed(counter, "sql.active_record", &block)

    queries
  end

  def assert_sql_queries(expected, &block)
    queries = sql_queries(&block)
    queries.count.must_equal(
      expected,
      "Expected #{expected} queries, but found #{queries.count}:\n#{queries.join("\n")}"
    )
  end
end

1voto

mahemoff Points 4879

Sur la base de la réponse de Jaime, ce qui suit prend en charge une assertion pour le nombre de requêtes jusqu'à présent dans le cas de test actuel, et enregistrera les déclarations en cas d'échec. Je pense qu'il est utile, d'un point de vue pragmatique, de combiner une vérification SQL comme celle-ci avec un test fonctionnel, car cela réduit l'effort de configuration.

class ActiveSupport::TestCase

   ActiveSupport::Notifications.subscribe('sql.active_record') do |name, started, finished, unique_id, payload|
     (@@queries||=[]) << payload unless payload[:name].in? %w(CACHE SCHEMA)
   end

   def assert_queries_count(expected_count, message=nil)
     assert_equal expected_count, @@queries.size,
       message||"Expected #{expected_count} queries, but #{@@queries.size} queries occurred.#{@@queries[0,20].join(' ')}"
   end

   # common setup in a super-class (or use Minitest::Spec etc to do it another way)
   def setup
     @@queries = []
   end

end

Utilisation :

def test_something
   post = Post.new('foo')
   assert_queries_count 1 # SQL performance check
   assert_equal "Under construction", post.body # standard functional check
end

Notez que l'assertion de requête doit avoir lieu immédiatement au cas où les autres assertions déclencheraient elles-mêmes des requêtes supplémentaires.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X