programing

실행된 쿼리 수 카운트

lastmemo 2023. 3. 12. 10:20
반응형

실행된 쿼리 수 카운트

특정 코드가 가능한 한 적은 수의 SQL 쿼리를 수행하는지 테스트하고 싶습니다.

ActiveRecord::TestCase나름의 것이 있는 것 같다assert_queries바로 그렇게 할 수 있는 방법이죠.하지만 Active Record에 패치를 적용하지 않기 때문에 별로 도움이 되지 않습니다.

RSpec 또는 Active Record는 코드 블록에서 실행된 SQL 쿼리 수를 공식적이고 공개적으로 카운트하는 방법을 제공합니까?

제 생각에 당신은 자신의 질문에 대해assert_queries단, 다음과 같습니다.

이 뒤에 있는 코드를 볼 것을 권장합니다.assert_queries쿼리 카운트에 사용할 수 있는 독자적인 방법을 구축하기 위해 사용합니다.여기서의 주요 매직은 다음과 같습니다.

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

오늘 아침 조금 손질해서 Active Record에서 쿼리 카운트를 하는 부분을 떼어내서 다음과 같이 생각해 냈습니다.

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를 올바르게 증가시킵니다.Query Counter(쿼리 카운터).query_count 변수. 단, 실제 쿼리에 한합니다.

아무튼 재밌었어요.당신에게 도움이 되길 바랍니다.

Ryan의 대본에 대한 나의 비전(약간 청소하고 매처로 감싼 것)은 누군가에게는 여전히 현실적이기를 바란다.

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/spec_matchers_limit.spec에 대한 설명

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)

다음은 Ryan과 Yuriy의 솔루션의 또 다른 공식입니다.그것은, 유리에의 유저에 의한 기능에 의한 것입니다.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
  • 유용한 오류 메시지
  • 실행 후 서브스크라이버를 삭제합니다.

(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

다음은 Jaime의 답변을 바탕으로 현재 테스트 케이스에서 지금까지의 쿼리 수에 대한 어설션을 지원하며, 실패 시 스테이트먼트를 기록합니다.이러한 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

쿼리 어설션은 다른 어설션 자체가 추가 쿼리를 트리거하는 경우 즉시 실행되어야 합니다.

다음은 주어진 패턴과 일치하는 쿼리를 쉽게 계산할 수 있는 버전입니다.

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

이 문제를 추상화하기 위해 sql_spy라는 작은 보석을 만들었습니다.

Gemfile에 추가하기만 하면 됩니다.

gem "sql_spy"

를 드드안 wrap드 wrap wrap wrap wrap wrap wrap wrap wrap wrap wrap 。SqlSpy.track { ... }:

queries = SqlSpy.track do
  # Some code that triggers ActiveRecord queries
  users = User.all
  posts = BlogPost.all
end

...어설션에서는 블록의 반환값을 사용합니다.

expect(queries.size).to eq(2)
expect(queries[0].sql).to eq("SELECT * FROM users;")
expect(queries[0].model_name).to eq("User")
expect(queries[0].select?).to be_true
expect(queries[0].duration).to eq(1.5)

유리의 솔루션을 기반으로 테이블별로 쿼리를 체크하는 기능을 추가했습니다.

# 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

사용.

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)

언급URL : https://stackoverflow.com/questions/5490411/counting-the-number-of-queries-performed

반응형