2011-03-30 35 views
39

Me gustaría comprobar que un determinado fragmento de código realiza la menor cantidad posible de consultas SQL.Contando el número de consultas realizadas

ActiveRecord::TestCase parece tener su propio método assert_queries, que hará exactamente eso. Pero como no estoy aplicando el parche a ActiveRecord, me sirve de poco.

¿RSpec o ActiveRecord proporcionan algún medio oficial y público de contar el número de consultas SQL realizadas en un bloque de código?

Respuesta

44

Creo que responde a su propia pregunta mencionando assert_queries, pero aquí va:

recomendaría echar un vistazo al código detrás assert_queries y utilizarlo para construir su propio método que se puede utilizar para contar las consultas. La principal magia en juego aquí es la siguiente línea:

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

que tenía un poco de un hojalatero esta mañana y arranqué las partes de ActiveRecord que hacen el recuento de consulta y se acercó con esto:

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 

Podrá hacer referencia al método ActiveRecord::Base.count_queries en cualquier lugar. Pasarlo un bloque en el que sus consultas se ejecutan y se devolverá el número de consultas que han sido ejecutadas:

ActiveRecord::Base.count_queries do 
    Ticket.first 
end 

Devuelve "1" para mí. Para realizar este trabajo: la puso en un archivo en lib/active_record/query_counter.rb y lo requiera en su archivo config/application.rb así:

require 'active_record/query_counter' 

¡listo!


Probablemente se requiera un poco de explicación. Cuando llamamos a esta línea:

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

Nos conectamos al pequeño marco de notificaciones de Rails 3. Es una pequeña y brillante adición a la última versión principal de Rails que nadie conoce realmente. Nos permite suscribir notificaciones de eventos dentro de Rails utilizando el método subscribe. Pasamos el evento en el que deseamos suscribirnos como primer argumento, luego cualquier objeto que responda a call como el segundo.

En este caso, cuando se ejecuta una consulta, nuestro pequeño contador de consultas aumentará diligentemente la variable ActiveRecord :: QueryCounter.query_count, pero solo para las consultas reales.

De todos modos, esto fue divertido. Espero que te sea útil.

+2

Excelente script. Si solo lo está utilizando para realizar pruebas, puede colocarlo en un archivo {spec | test} /support/query_counter.rb. Mantenga la carpeta lib para la lógica de la aplicación. – Forrest

+0

Cualquier forma de hacer que esto funcione en .18.7 –

+0

Para aquellos que buscan un compatibilizador RSpec, esta respuesta se ha convertido en una gema: ['rspec-sqlimit'] (https://github.com/nepalez/rspec-sqlimit) . –

20

Mi visión del guión de Ryan (limpiado un poco y envuelto en una matcher), espero que sigue siendo real para alguien:

pongo esto a las especificaciones/soporte/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 

y esto a 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 

Uso:

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

Actualizaciones menores para RSpec 3 en [esta esencia] (https://gist.github.com/rsutphin/af06c9e3dadf658d2293). –

10

Aquí es otra formulación de Ryan y la solución de Yuriy eso es sólo una función que añadir a su 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 

uso es simplemente:

c = count_queries do 
    SomeModel.first 
end 
0

Según la respuesta de Jaime, lo siguiente respalda una afirmación para el número de consultas hasta el momento en el caso de prueba actual, y registrará las declaraciones en caso de error. Creo que es útil combinar pragmáticamente un cheque SQL como este con una prueba funcional, ya que reduce el esfuerzo de configuración.

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 

Uso:

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

nota la afirmación de consulta debe ocurrir inmediatamente en caso de que las otras afirmaciones a sí mismos desencadenan consultas adicionales.

1

Aquí hay una versión que hace que sea fácil contar las consultas que coinciden con un patrón determinado.

module QueryCounter 

    def self.count_selects(&block) 
    count(pattern: /^(\s+)?SELECT/, &block) 
    end 

    def self.count(pattern: /(.*?)/, &block) 
    counter = 0 

    callback = ->(name, started, finished, callback_id, payload) { 
     counter += 1 if payload[:sql].match(pattern) 
     # puts "match? #{!!payload[:sql].match(pattern)}: #{payload[:sql]}" 
    } 

    # http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html 
    ActiveSupport::Notifications.subscribed(callback, "sql.active_record", &block) 

    counter 
    end 

end 

Uso:

test "something" do 
    query_count = count_selects { 
    Thing.first 
    Thing.create!(size: "huge") 
    } 
    assert_equal 1, query_count 
end 
2
  • mensaje de error útiles
  • elimina los suscriptores después de la ejecución

(basado en la respuesta 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 
Cuestiones relacionadas