Подсчет количества выполненных запросов
Я бы хотел проверить, что определенный фрагмент кода выполняет как можно меньше SQL-запросов.
ActiveRecord::TestCase
похоже, имеет свой собственный метод assert_queries
, который будет делать именно это. Но так как я не исправляю ActiveRecord, он мне мало подходит.
Предоставляет ли RSpec или ActiveRecord какие-либо официальные общедоступные средства подсчета количества SQL-запросов, выполняемых в блоке кода?
Ответы
Ответ 1
Думаю, вы ответили на свой вопрос, отметив 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, но только для реальных запросов.
В любом случае, это было весело. Надеюсь, это пригодится вам.
Ответ 2
Мое видение Ryan script (очищено немного и завернуто в матчи), надеюсь, что это по-прежнему актуально для кого-то:
Я помещал это в 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/above_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)
Ответ 3
Здесь другая формулировка решения Райана и Юрия, которая просто добавляет функцию к вашему 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
Ответ 4
- полезное сообщение об ошибке
- удаляет подписчиков после выполнения
(на основании ответа Хайме Чама)
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
Ответ 5
Здесь приведена версия, которая упрощает подсчет запросов, соответствующих данному шаблону.
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
Ответ 6
Основываясь на ответе Хайме, следующее поддерживает утверждение количества запросов в текущем тестовом случае и будет записывать заявления в случае сбоя. Я думаю, что полезно прагматично объединить проверку 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
Обратите внимание, что утверждение запроса должно происходить немедленно, если другие утверждения сами запускают дополнительные запросы.
Ответ 7
Я добавил возможность проверять запросы по таблице на основе решения Юрия
# spec/support/query_counter.rb
require 'support/matchers/query_limit'
module ActiveRecord
class QueryCounter
attr_reader :queries
def initialize
@queries = Hash.new 0
end
def to_proc
lambda(&method(:callback))
end
def callback(name, start, finish, message_id, values)
sql = values[:sql]
if sql.include? 'SAVEPOINT'
table = :savepoints
else
finder = /select.+"(.+)"\..+from/i if sql.include? 'SELECT'
finder = /insert.+"(.+)".\(/i if sql.include? 'INSERT'
finder = /update.+"(.+)".+set/i if sql.include? 'UPDATE'
finder = /delete.+"(.+)" where/i if sql.include? 'DELETE'
table = sql.match(finder)&.send(:[],1)&.to_sym
end
@queries[table] += 1 unless %w(CACHE SCHEMA).include?(values[:name])
return @queries
end
def query_count(table = nil)
if table
@queries[table]
else
@queries.values.sum
end
end
end
end
Сопоставители RSpec выглядят как
# spec/support/matchers/query_limit.rb
RSpec::Matchers.define :exceed_query_limit do |expected, table|
supports_block_expectations
match do |block|
query_count(table, &block) > expected
end
def query_count(table, &block)
@counter = ActiveRecord::QueryCounter.new
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
@counter.query_count table
end
failure_message_when_negated do |actual|
queries = 'query'.pluralize expected
table_name = table.to_s.singularize.humanize.downcase if table
out = "expected to run a maximum of #{expected}"
out += " #{table_name}" if table
out += " #{queries}, but got #{@counter.query_count table}"
end
end
RSpec::Matchers.define :meet_query_limit do |expected, table|
supports_block_expectations
match do |block|
if expected.is_a? Hash
results = queries_count(table, &block)
expected.all? { |table, count| results[table] == count }
else
query_count(&block) == expected
end
end
def queries_count(table, &block)
@counter = ActiveRecord::QueryCounter.new
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
@counter.queries
end
def query_count(&block)
@counter = ActiveRecord::QueryCounter.new
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
@counter.query_count
end
def message(expected, table, negated = false)
queries = 'query'.pluralize expected
if expected.is_a? Hash
results = @counter.queries
table, expected = expected.find { |table, count| results[table] != count }
end
table_name = table.to_s.singularize.humanize.downcase if table
out = 'expected to'
out += ' not' if negated
out += " run exactly #{expected}"
out += " #{table_name}" if table
out += " #{queries}, but got #{@counter.query_count table}"
end
failure_message do |actual|
message expected, table
end
failure_message_when_negated do |actual|
message expected, table, true
end
end
Usage
Usage
expect { MyModel.do_the_queries }.to_not meet_query_limit(3)
expect { MyModel.do_the_queries }.to meet_query_limit(3)
expect { MyModel.do_the_queries }.to meet_query_limit(my_models: 2, other_tables: 1)