Подсчет количества выполненных запросов - PullRequest
40 голосов
/ 30 марта 2011

Я хотел бы проверить, что определенный фрагмент кода выполняет как можно меньше запросов SQL.

ActiveRecord::TestCase, кажется, имеет свой собственный метод assert_queries, который будет делать именно это. Но так как я не исправляю ActiveRecord, он мне не нужен.

Предоставляет ли RSpec или ActiveRecord какие-либо официальные, общедоступные средства подсчета количества запросов SQL, выполненных в блоке кода?

Ответы [ 6 ]

47 голосов
/ 31 марта 2011

Я думаю, что вы ответили на свой вопрос, упомянув assert_queries, но здесь говорится:

Я бы порекомендовал взглянуть на код, стоящий за assert_queries, и использовать его для создания собственного метода, который вы можетеиспользовать для подсчета запросов.Главное волшебство, которое здесь задействовано, - это строка:

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

Сегодня утром я немного повозился и вытащил части ActiveRecord, которые выполняют подсчет запросов, и придумал следующее:

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

Вы сможете ссылаться на метод ActiveRecord::Base.count_queries в любом месте.Передайте ему блок, в котором выполняются ваши запросы, и он вернет количество выполненных запросов:

ActiveRecord::Base.count_queries do
  Ticket.first
end

Возвращает «1» для меня.Чтобы сделать это: поместите его в файл на lib/active_record/query_counter.rb и укажите его в файле config/application.rb, например:

require 'active_record/query_counter'

Привет!


Немногообъяснения, вероятно, требуется.Когда мы вызываем эту строку:

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

Мы подключаемся к небольшой структуре уведомлений Rails 3.Это блестящее небольшое дополнение к последней основной версии Rails, о котором никто не знает.Это позволяет нам подписываться на уведомления о событиях в Rails, используя метод subscribe.Мы передаем событие, на которое хотим подписаться, в качестве первого аргумента, а затем любой объект, который отвечает на call в качестве второго.

В этом случае, когда запрос выполняется, наш маленький счетчик запросов должным образом увеличивает ActiveRecordПеременная :: QueryCounter.query_count, но только для реальных запросов.

В любом случае, это было весело.Надеюсь, это пригодится вам.

21 голосов
/ 17 ноября 2012

Мое видение сценария Райана (немного очищенного и завернутого в сопоставление), надеюсь, оно все еще актуально для кого-то:

Я поместил это в 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

и это в spec / support / matchers / exce_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

Использование:

expect { MyModel.do_the_queries }.to_not exceed_query_limit(2)
11 голосов
/ 13 марта 2014

Вот еще одна формулировка решения Райана и Юрия, это просто функция, которую вы добавляете к своему 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

Использование всего лишь:

c = count_queries do
  SomeModel.first
end
5 голосов
/ 05 мая 2017
  • полезное сообщение об ошибке
  • удаляет подписчиков после исполнения

(основываясь на ответе Хайме Чама)

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
1 голос
/ 14 октября 2015

Вот версия, которая облегчает подсчет запросов, соответствующих заданному шаблону.

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

Использование:

test "something" do
  query_count = count_selects {
    Thing.first
    Thing.create!(size: "huge")
  }
  assert_equal 1, query_count
end
0 голосов
/ 30 апреля 2015

Основываясь на ответе Хайме, следующее поддерживает утверждение о количестве запросов на текущий момент в текущем тестовом примере и будет регистрировать операторы в случае сбоя.Я думаю, что прагматично полезно сочетать такую ​​проверку SQL с функциональным тестом, поскольку это сокращает усилия по установке.

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

Использование:

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

Обратите внимание, что утверждение запроса должно произойти немедленнов случае, если другие утверждения сами вызывают дополнительные запросы.

...