From f458b376c545d8eae5189d1ecc16245cafe7e796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Tue, 6 Feb 2007 21:16:07 +0000 Subject: Introducing Model.cache { ... } for the occasional query caching needs. ( fantastic to reduce the 200 SELECT * from accounts WHERE id=1 queries in your views ) git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@6138 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 3 + activerecord/lib/active_record/query_cache.rb | 51 +++++++++++------ activerecord/test/abstract_unit.rb | 36 ++++++------ activerecord/test/query_cache_test.rb | 80 +++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 34 deletions(-) create mode 100644 activerecord/test/query_cache_test.rb diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 741a546993..ba42776c7f 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,8 @@ *SVN* +* Reworked David's query cache to be available as Model.cache {...}. For the duration of the block no select query should be run more then once. Any inserts/deletes/executes will flush the whole cache however [Tobias Luetke] + Task.cache { Task.find(1); Task.find(1) } #=> 1 query + * When dealing with SQLite3, use the table_info pragma helper, so that the bindings can do some translation for when sqlite3 breaks incompatibly between point releases. [Jamis Buck] * Oracle: fix lob and text default handling. #7344 [gfriedrich, Michael Schoen] diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index e79b3e05a7..415b25e096 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -6,7 +6,7 @@ module ActiveRecord end def clear_query_cache - @query_cache = {} + @query_cache.clear end def select_all(sql, name = nil) @@ -16,14 +16,27 @@ module ActiveRecord def select_one(sql, name = nil) @query_cache[sql] ||= @connection.select_one(sql, name) end + + def select_values(sql, name = nil) + (@query_cache[sql] ||= @connection.select_values(sql, name)).dup + end + + def select_value(sql, name = nil) + @query_cache[sql] ||= @connection.select_value(sql, name) + end + + def execute(sql, name = nil) + clear_query_cache + @connection.execute(sql, name) + end def columns(table_name, name = nil) @query_cache["SHOW FIELDS FROM #{table_name}"] ||= @connection.columns(table_name, name) end - def insert(sql, name = nil, pk = nil, id_value = nil) + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) clear_query_cache - @connection.insert(sql, name, pk, id_value) + @connection.insert(sql, name, pk, id_value, sequence_name) end def update(sql, name = nil) @@ -41,24 +54,26 @@ module ActiveRecord @connection.send(method, *arguments, &proc) end end - + class Base # Set the connection for the class with caching on class << self - alias_method :connection_without_query_cache=, :connection= - - def connection=(spec) - if spec.is_a?(ConnectionSpecification) and spec.config[:query_cache] - spec = QueryCache.new(self.send(spec.adapter_method, spec.config)) - end - self.connection_without_query_cache = spec + alias_method :connection_without_query_cache, :connection + + def query_caches + (Thread.current[:query_cache] ||= {}) + end + + def cache + query_caches[self] = QueryCache.new(connection) + yield + ensure + query_caches[self] = nil + end + + def connection + query_caches[self] || connection_without_query_cache end end - end - - class AbstractAdapter #:nodoc: - # Stub method to be able to treat the connection the same whether the query cache has been turned on or not - def clear_query_cache - end - end + end end diff --git a/activerecord/test/abstract_unit.rb b/activerecord/test/abstract_unit.rb index 3fd94afc00..5a15871279 100755 --- a/activerecord/test/abstract_unit.rb +++ b/activerecord/test/abstract_unit.rb @@ -36,16 +36,10 @@ class Test::Unit::TestCase #:nodoc: end def assert_queries(num = 1) - ActiveRecord::Base.connection.class.class_eval do - self.query_count = 0 - alias_method :execute, :execute_with_query_counting - end + $query_count = 0 yield ensure - ActiveRecord::Base.connection.class.class_eval do - alias_method :execute, :execute_without_query_counting - end - assert_equal num, ActiveRecord::Base.connection.query_count, "#{ActiveRecord::Base.connection.query_count} instead of #{num} queries were executed." + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." end def assert_no_queries(&block) @@ -60,16 +54,26 @@ def current_adapter?(*types) end end -ActiveRecord::Base.connection.class.class_eval do - cattr_accessor :query_count +def uses_mocha(test_name) + require 'mocha' + require 'stubba' + yield +rescue LoadError + $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again." +end - # Array of regexes of queries that are not counted against query_count - @@ignore_list = [/^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/] +ActiveRecord::Base.connection.class.class_eval do + + if not (const_get('IGNORED_SQL') rescue nil) + IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/] + + def execute_with_counting(sql, name = nil, &block) + $query_count ||= 0 + $query_count += 1 unless IGNORED_SQL.any? { |r| sql =~ r } + execute_without_counting(sql, name, &block) + end - alias_method :execute_without_query_counting, :execute - def execute_with_query_counting(sql, name = nil, &block) - self.query_count += 1 unless @@ignore_list.any? { |r| sql =~ r } - execute_without_query_counting(sql, name, &block) + alias_method_chain :execute, :counting end end diff --git a/activerecord/test/query_cache_test.rb b/activerecord/test/query_cache_test.rb new file mode 100644 index 0000000000..5326351483 --- /dev/null +++ b/activerecord/test/query_cache_test.rb @@ -0,0 +1,80 @@ +require 'abstract_unit' +require 'fixtures/topic' +require 'fixtures/reply' +require 'fixtures/task' + + +class QueryCacheTest < Test::Unit::TestCase + fixtures :tasks + + + def test_find_queries + assert_queries(2) { Task.find(1); Task.find(1) } + end + + def test_find_queries_with_cache + Task.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + end + + def test_find_queries_with_cache + Task.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + end + + def test_cache_is_scoped_on_actual_class_only + Task.cache do + assert_queries(2) { Topic.find(1); Topic.find(1) } + end + end +end + +uses_mocha('QueryCacheExpiryTest') do + +class QueryCacheExpiryTest < Test::Unit::TestCase + fixtures :tasks + + def test_find + ActiveRecord::QueryCache.any_instance.expects(:clear_query_cache).times(0) + + Task.cache do + Task.find(1) + end + end + + def test_save + ActiveRecord::QueryCache.any_instance.expects(:clear_query_cache).times(1) + + Task.cache do + Task.find(1).save + end + end + + def test_destroy + ActiveRecord::QueryCache.any_instance.expects(:clear_query_cache).at_least_once + + Task.cache do + Task.find(1).destroy + end + end + + def test_create + ActiveRecord::QueryCache.any_instance.expects(:clear_query_cache).times(1) + + Task.cache do + Task.create! + end + end + + def test_new_save + ActiveRecord::QueryCache.any_instance.expects(:clear_query_cache).times(1) + + Task.cache do + Task.new.save + end + end +end + +end \ No newline at end of file -- cgit v1.2.3