【问题标题】:Counting the number of queries performed计算执行的查询数
【发布时间】:2011-03-30 18:15:10
【问题描述】:

我想测试某段代码是否执行尽可能少的 SQL 查询。

ActiveRecord::TestCase 似乎有自己的assert_queries 方法,它可以做到这一点。但由于我没有修补 ActiveRecord,它对我来说没什么用。

RSpec 或 ActiveRecord 是否提供任何官方、公开的方法来计算代码块中执行的 SQL 查询的数量?

【问题讨论】:

    标签: mysql ruby-on-rails rspec rails-activerecord


    【解决方案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 最新主要版本的一个闪亮的小补充,没有人真正知道。它允许我们使用subscribe 方法订阅Rails 中的事件通知。我们将要订阅的事件作为第一个参数传入,然后将响应 call 的任何对象作为第二个参数传入。

    在这种情况下,当执行查询时,我们的小查询计数器将尽职尽责地增加 ActiveRecord::QueryCounter.query_count 变量,但仅限于真正的查询。

    无论如何,这很有趣。希望对你有用。

    【讨论】:

    • 很棒的剧本。如果您仅将其用于测试,您可以将其放入 {spec|test}/support/query_counter.rb 文件中。保留应用程序逻辑的 lib 文件夹。
    • 对于那些寻找 RSpec 匹配器的人来说,这个答案已经变成了一个宝石:rspec-sqlimit
    【解决方案2】:

    我对 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/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
    

    用法:

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

    【讨论】:

    【解决方案3】:

    这是 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
    

    【讨论】:

      【解决方案4】:
      • 有用的错误信息
      • 执行后删除订阅者

      (基于 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
      

      【讨论】:

        【解决方案5】:

        根据 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
        

        注意查询断言应该立即发生,以防其他断言本身触发额外的查询。

        【讨论】:

          【解决方案6】:

          这是一个可以轻松计算与给定模式匹配的查询的版本。

          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
          

          【讨论】:

            【解决方案7】:

            我最终创建了一个小宝石来抽象这个问题:sql_spy

            只需将其添加到您的 Gemfile 中:

            gem "sql_spy"
            

            将您的代码封装在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)
            

            【讨论】:

              【解决方案8】:

              我添加了根据 Yuriy 的解决方案检查每个表的查询的功能

              # 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)
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2012-11-27
                • 2018-10-03
                • 1970-01-01
                • 1970-01-01
                • 2015-11-17
                • 2016-05-05
                • 2017-06-14
                • 2017-05-10
                相关资源
                最近更新 更多