Skip to content

Commit

Permalink
Enable caching by default for all enumerations
Browse files Browse the repository at this point in the history
Allow caching to be turned off on an application-wide basis
Allow cache store to be configurable
  • Loading branch information
obrie committed Apr 28, 2009
1 parent 9f65588 commit 34fa878
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 49 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rdoc
@@ -1,5 +1,8 @@
== master

* Enable caching by default for all enumerations
* Allow caching to be turned off on an application-wide basis
* Allow cache store to be configurable
* Automatically trigger in-memory caching of the enumeration's table when bootstrapping
* Add #bootstrap for automatically synchronizing the records in an enumeration's table
* Improve serialization performance
Expand Down
63 changes: 38 additions & 25 deletions lib/enumerate_by.rb
Expand Up @@ -7,6 +7,15 @@
# numerical order. This extension provides a general technique for using
# ActiveRecord classes to define enumerations.
module EnumerateBy
# Whether to enable enumeration caching (default is true)
mattr_accessor :perform_caching
self.perform_caching = true

# The cache store to use for queries within enumerations (default is a
# memory store)
mattr_accessor :cache_store
self.cache_store = ActiveSupport::Cache::MemoryStore.new

module MacroMethods
def self.extended(base) #:nodoc:
base.class_eval do
Expand All @@ -27,6 +36,10 @@ def self.extended(base) #:nodoc:
# defined for the given attribute since all records must have this value
# in order to be properly enumerated.
#
# Configuration options:
# * <tt>:cache</tt> - Whether to cache all finder queries for this
# enumeration. Default is true.
#
# == Defining enumerators
#
# The enumerators of the class uniquely identify each record in the
Expand All @@ -43,9 +56,8 @@ def self.extended(base) #:nodoc:
# Color['red'] # => #<Color id: 1, name: "red">
# Color['green'] # => #<Color id: 2, name: "green">
#
# When used in association with #bootstrap, these records are cached so
# there is no performance hit and the same object can be compared against
# itself, i.e. Color['red'] == Color['red']
# When caching is enabled, these lookup queries are cached so that there
# is no performance hit.
#
# == Associations
#
Expand Down Expand Up @@ -74,16 +86,20 @@ def self.extended(base) #:nodoc:
# Car.all(:conditions => {:color => 'red'})
#
# For more information about finders, see EnumerateBy::Extensions::BaseConditions.
def enumerate_by(attribute = :name)
def enumerate_by(attribute = :name, options = {})
options.reverse_merge!(:cache => true)
options.assert_valid_keys(:cache)

extend EnumerateBy::ClassMethods
include EnumerateBy::InstanceMethods

# The attribute representing a record's enumerator
cattr_accessor :enumerator_attribute
self.enumerator_attribute = attribute

# In-memory cache of database queries for this model
cattr_accessor :enumerator_cache
# Whether to perform caching of enumerators within finder queries
cattr_accessor :perform_enumerator_caching
self.perform_enumerator_caching = options[:cache]

validates_presence_of attribute
validates_uniqueness_of attribute
Expand All @@ -96,12 +112,6 @@ def enumeration?
end

module ClassMethods
def self.extended(base) #:nodoc:
class << base
alias_method_chain :find_by_sql, :enumerations
end
end

# Does this class define an enumeration? Always true.
def enumeration?
true
Expand Down Expand Up @@ -133,17 +143,16 @@ def find_by_enumerator(enumerator)
first(:conditions => {enumerator_attribute => enumerator})
end

# Adds support for looking up records from an in-memory process cache for
# the model before querying the database.
#
# This allows for models that have been bootstrapped (and, thus, will
# never change) to be permanently cached avoiding unnecessary queries to
# the database.
def find_by_sql_with_enumerations(sql)
if enumerator_cache
enumerator_cache.fetch(sql) { find_by_sql_without_enumerations(sql) }
# Adds support for looking up records from the enumeration cache for
# before querying the database.
#
# This allows for enumerations to permanently cache find queries, avoiding
# unnecessary lookups in the database.
def find_by_sql(sql)
if EnumerateBy.perform_caching && perform_enumerator_caching
EnumerateBy.cache_store.fetch(sql) { super }
else
find_by_sql_without_enumerations(sql)
super
end
end

Expand Down Expand Up @@ -195,6 +204,10 @@ def find_by_sql_with_enumerations(sql)
# only be synchronized if the attribute is nil in the database.
# Otherwise, any changes to that column remain there.
def bootstrap(*records)
# Temporarily turn off caching
perform_caching = perform_enumerator_caching
self.perform_enumerator_caching = false

# Remove records that are no longer being used
delete_all(['id NOT IN (?)', records.map {|record| record[:id]}])
existing = all.inject({}) {|existing, record| existing[record.id] = record; existing}
Expand All @@ -217,10 +230,10 @@ def bootstrap(*records)
record
end

# Enable the query cache
self.enumerator_cache = ActiveSupport::Cache::MemoryStore.new

records
ensure
# Revert caching back to its original value
self.perform_enumerator_caching = perform_caching
end
end

Expand Down
2 changes: 2 additions & 0 deletions test/test_helper.rb
Expand Up @@ -24,3 +24,5 @@ def execute_with_query_record(sql, name = nil, &block)

alias_method_chain :execute, :query_record
end

EnumerateBy.perform_caching = false
81 changes: 57 additions & 24 deletions test/unit/enumerate_by_test.rb
@@ -1,5 +1,15 @@
require File.dirname(__FILE__) + '/../test_helper'

class EnumerateByTest < ActiveRecord::TestCase
def test_should_have_a_cache_store
assert_instance_of ActiveSupport::Cache::MemoryStore, EnumerateBy.cache_store
end

def test_should_raise_exception_if_invalid_option_specified
assert_raise(ArgumentError) { Color.enumerate_by(:id, :invalid => true) }
end
end

class ModelWithoutEnumerationTest < ActiveRecord::TestCase
def test_should_not_be_an_enumeration
assert !Car.enumeration?
Expand Down Expand Up @@ -119,6 +129,53 @@ def test_should_stringify_enumerator
end
end

class EnumerationWithCachingTest < ActiveRecord::TestCase
def setup
@red = create_color(:name => 'red')

EnumerateBy.perform_caching = true
end

def test_should_perform_enumerator_caching
assert Color.perform_enumerator_caching
end

def test_should_cache_all_finder_queries
assert_queries(1) { Color.find(@red.id) }
assert_queries(0) { Color.find(@red.id) }

assert_queries(1) { Color.all }
assert_queries(0) { Color.all }
end

def teardown
EnumerateBy.perform_caching = false
end
end

class EnumerationWithoutCachingTest < ActiveRecord::TestCase
def setup
@red = create_color(:name => 'red')

EnumerateBy.perform_caching = true
@original_perform_caching = Color.perform_enumerator_caching
Color.perform_enumerator_caching = false
end

def test_should_not_cache_finder_queries
assert_queries(1) { Color.find(@red.id) }
assert_queries(1) { Color.find(@red.id) }

assert_queries(1) { Color.all }
assert_queries(1) { Color.all }
end

def teardown
EnumerateBy.perform_caching = false
Color.perform_enumerator_caching = @original_perform_caching
end
end

class EnumerationBootstrappedTest < ActiveRecord::TestCase
def setup
@red, @green = Color.bootstrap(
Expand All @@ -142,22 +199,6 @@ def test_should_create_records
assert_equal @green, Color.find(2)
assert_equal 'green', @green.name
end

def test_should_enable_enumerator_cache
assert_not_nil Color.enumerator_cache
end

def test_should_cache_all_finder_queries
assert_queries(1) { Color.find(1) }
assert_queries(0) { Color.find(1) }

assert_queries(1) { Color.all }
assert_queries(0) { Color.all }
end

def teardown
Color.enumerator_cache = nil
end
end

class EnumerationBootstrappedWithExistingRecordsTest < ActiveRecord::TestCase
Expand All @@ -178,10 +219,6 @@ def test_should_synchronize_all_attributes
assert_equal 'red', @red.name
assert_equal 'green', @green.name
end

def teardown
Color.enumerator_cache = nil
end
end

class EnumerationBootstrappedWithDefaultsTest < ActiveRecord::TestCase
Expand Down Expand Up @@ -210,8 +247,4 @@ def test_should_not_update_default_attributes_if_defined
def test_should_update_default_attributes_if_not_defined
assert_equal '#00ff00', @green.html
end

def teardown
Color.enumerator_cache = nil
end
end

0 comments on commit 34fa878

Please sign in to comment.