跟踪一下两次查询所使用的query cache

class StudentsController < ApplicationController
  def show
    id = params[:id]
    @student =binding.trace_tree(htmp: 'ar/query_cache') do
      Student.find id
      Student.find id
    end
  end
end


然后执行

[2] pry(main)> app.get '/students/2'
Started GET "/students/2" for 127.0.0.1 at 2017-11-15 21:57:28 +0800
   (0.1ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by StudentsController#show as HTML
  Parameters: {"id"=>"2"}
  Student Load (75.4ms)  SELECT  "students".* FROM "students" WHERE "students"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  CACHE Student Load (25.6ms)  SELECT  "students".* FROM "students" WHERE "students"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering students/show.html.erb within layouts/application
  Rendered students/show.html.erb within layouts/application (5.4ms)


得调用栈如下

20171115_215728_424_query_cache.html

检查第二个find,可看到cached_find_by_statement、StatementCache、cache_sql、query_cache.rb这些字眼。这个过程经历的代码大概如下

activerecord-5.1.4/lib/active_record/core.rb

def cached_find_by_statement(key, &block)
  cache = @find_by_statement_cache[connection.prepared_statements]
  cache[key] || cache.synchronize {
    cache[key] ||= StatementCache.create(connection, &block)
  }
end


activerecord-5.1.4/lib/active_record/statement_cache.rb

def execute(params, klass, connection, &block)
  bind_values = bind_map.bind params

  sql = query_builder.sql_for bind_values, connection

  klass.find_by_sql(sql, bind_values, preparable: true, &block)
end


activerecord-5.1.4/lib/active_record/connection_adapters/abstract/query_cache.rb

def select_all(arel, name = nil, binds = [], preparable: nil)
  if @query_cache_enabled && !locked?(arel)
    arel, binds = binds_from_relation arel, binds
    sql = to_sql(arel, binds)
    cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
  else
    super
  end
end

private

  def cache_sql(sql, name, binds)
    @lock.synchronize do
      result =
        if @query_cache[sql].key?(binds)
          ActiveSupport::Notifications.instrument(
            "sql.active_record",
            sql: sql,
            binds: binds,
            name: name,
            connection_id: object_id,
            cached: true,
          )
          @query_cache[sql][binds]
        else
          @query_cache[sql][binds] = yield
        end
      result.dup
    end
  end


在query_cache.rb中,调用cache_sql的就只有select_all,莫非所有查询最终都会去到select_all?

于是尝试一个稍微复杂的sql

[4] pry(main)> binding.trace_tree(htmp: 'ar/joins'){ Human.joins(:cells, :spirit).where('cells.id >= 18').to_a }
  Human Load (31.3ms)  SELECT "humen".* FROM "humen" INNER JOIN "cells" ON "cells"."human_id" = "humen"."id" INNER JOIN "spirits" ON "spirits"."human_id" = "humen"."id" WHERE (cells.id >= 18)
=> [#<human:0x007f3f2bab5c18 id:="" 9,="" name:="" "ken",="" created_at:="" sun,="" 05="" nov="" 2017="" 13:07:10="" utc="" +00:00,="" updated_at:="" tue,="" 07="" nov="" 2017="" 14:06:53="" utc="" +00:00="">,
#<human:0x007f3f2f286638 id:="" 9,="" name:="" "ken",="" created_at:="" sun,="" 05="" nov="" 2017="" 13:07:10="" utc="" +00:00,="" updated_at:="" tue,="" 07="" nov="" 2017="" 14:06:53="" utc="" +00:00="">]</human:0x007f3f2f286638></human:0x007f3f2bab5c18>


完整调用栈如下

20171115_223217_038_joins.html

确实是的


那么@query_cache_enabled是怎么设为true的呢?为了得知,加入puts caller

module ActiveRecord
  module ConnectionAdapters
    module QueryCache

      def enable_query_cache!
        puts caller
        @query_cache_enabled = true
      end


再rails s访问,得

Started GET "/students/2" for 192.168.0.103 at 2017-11-15 23:42:01 +0800
Cannot render console from 192.168.0.103! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by StudentsController#show as HTML
  Parameters: {"id"=>"2"}
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/abstract/query_cache.rb:131:in `configure_query_cache!'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:413:in `block in make_lambda'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:270:in `block in simple'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:511:in `block in invoke_after'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:511:in `each'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:511:in `invoke_after'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:132:in `run_callbacks'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:827:in `_run_checkout_callbacks'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:807:in `checkout_and_verify'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:500:in `checkout'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:374:in `connection'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:931:in `retrieve_connection'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_handling.rb:116:in `retrieve_connection'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/connection_handling.rb:88:in `connection'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/core.rb:301:in `cached_find_by_statement'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.4/lib/active_record/core.rb:183:in `find'
# ...


这里获取数据库连接的checkout方法,已在query_cache.rb中定义了configure_query_cache!的callback

def connection
  @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
end

def checkout(checkout_timeout = @checkout_timeout)
  checkout_and_verify(acquire_connection(checkout_timeout))
end

def checkout_and_verify(c)
  c._run_checkout_callbacks do
    c.verify!
  end
  c
rescue
  remove c
  c.disconnect!
  raise
end


那么,为什么在console直接查询(非app.get ...)又没有query cache呢?

首先,回调configure_query_cache!是根据connection_pool是否query_cache_enabled来设置connection的enable_query_cache的

# activerecord-5.1.4/lib/active_record/connection_adapters/abstract/query_cache.rb
def configure_query_cache!
  enable_query_cache! if pool.query_cache_enabled
end


而connection_pool所include的ConnectionPoolConfiguration要靠有人调用enable_query_cache!来设置缓存开关

# activerecord-5.1.4/lib/active_record/connection_adapters/abstract/query_cache.rb
module ActiveRecord                                                                                                                                                                                                                     
  module ConnectionAdapters
    module QueryCache

      module ConnectionPoolConfiguration
        def enable_query_cache!
          @query_cache_enabled[connection_cache_key(Thread.current)] = true
          connection.enable_query_cache! if active_connection?
        end

        def query_cache_enabled
          @query_cache_enabled[connection_cache_key(Thread.current)]
        end
      end


搜索一下enable_query_cache!,会发现它是hook的

# activerecord-5.1.4/lib/active_record/query_cache.rb
module ActiveRecord
  class QueryCache          
    def self.run
      caching_pool = ActiveRecord::Base.connection_pool
      caching_was_enabled = caching_pool.query_cache_enabled

      caching_pool.enable_query_cache!                                                                                                                                                                                                  

      [caching_pool, caching_was_enabled]
    end          

    def self.install_executor_hooks(executor = ActiveSupport::Executor)
      executor.register_hook(self)
    end
  end
end


于是,在run中加入puts caller,再次rails c并app.get

[1] pry(main)> app.get '/students/2'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/execution_wrapper.rb:25:in `before'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:413:in `block in make_lambda'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:197:in `block (2 levels) in halting'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:601:in `block (2 levels) in default_terminator'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:600:in `catch'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:600:in `block in default_terminator'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:198:in `block in halting'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:507:in `block in invoke_before'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:507:in `each'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:507:in `invoke_before'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/callbacks.rb:130:in `run_callbacks'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/execution_wrapper.rb:108:in `run!'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/execution_wrapper.rb:70:in `block in run!'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/execution_wrapper.rb:67:in `tap'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/execution_wrapper.rb:67:in `run!'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.4/lib/action_dispatch/middleware/executor.rb:10:in `call'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.4/lib/action_dispatch/middleware/static.rb:125:in `call'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/sendfile.rb:111:in `call'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.1.4/lib/rails/engine.rb:522:in `call'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-test-0.7.0/lib/rack/mock_session.rb:30:in `request'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-test-0.7.0/lib/rack/test.rb:249:in `process_request'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-test-0.7.0/lib/rack/test.rb:125:in `request'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.4/lib/action_dispatch/testing/integration.rb:261:in `process'
/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.4/lib/action_dispatch/testing/integration.rb:16:in `get'
(pry):1:in `
'


可以看出这个回调在某层middleware被触发

# actionpack-5.1.4/lib/action_dispatch/middleware/executor.rb
module ActionDispatch
  class Executor
    def initialize(app, executor)
      @app, @executor = app, executor
    end

    def call(env)
      state = @executor.run!
      begin
        response = @app.call(env)
        returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
      ensure
        state.complete! unless returned              
      end                                                                                           
    end                                             
  end                                             
end


因此,只有经过rack才会设置query cache,单单在console中是没打开这一功能的(当然手动enable是可以)