Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added unique index cache and full table cache

  • Loading branch information...
commit 4ac2e324bd2f88ca048dfbdfa18202a3aeb13a11 1 parent 86e2977
@orslumen authored
View
2  lib/record_cache.rb
@@ -1,6 +1,6 @@
# Record Cache files
["query", "version_store", "multi_read",
- "strategy/util", "strategy/base", "strategy/id_cache", "strategy/index_cache", "strategy/request_cache",
+ "strategy/util", "strategy/base", "strategy/request_cache", "strategy/unique_index_cache", "strategy/full_table_cache", "strategy/index_cache",
"statistics", "dispatcher", "base"].each do |file|
require File.dirname(__FILE__) + "/record_cache/#{file}.rb"
end
View
9 lib/record_cache/datastore/active_record_30.rb
@@ -275,8 +275,15 @@ def update_all_with_record_cache(updates, conditions = nil, options = {})
if record_cache?
# when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
unless conditions || options.present? || @limit_value.present? != @order_values.present?
+ # get all attributes that contian a unique index for this model
+ unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
# go straight to SQL result (without instantiating records) for optimal performance
- connection.execute(select('id').to_sql).each{ |row| record_cache.invalidate(:id, (row.is_a?(Hash) ? row['id'] : row.first).to_i ) }
+ connection.execute(select(unique_index_attributes.map(&:to_s).join(',')).to_sql).each do |row|
+ # invalidate the unique index for all attributes
+ unique_index_attributes.each_with_index do |attribute, index|
+ record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]) )
+ end
+ end
end
end
View
9 lib/record_cache/datastore/active_record_31.rb
@@ -287,8 +287,15 @@ def update_all_with_record_cache(updates, conditions = nil, options = {})
if record_cache?
# when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
unless conditions || options.present? || @limit_value.present? != @order_values.present?
+ # get all attributes that contian a unique index for this model
+ unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
# go straight to SQL result (without instantiating records) for optimal performance
- connection.execute(select('id').to_sql).each{ |row| record_cache.invalidate(:id, (row.is_a?(Hash) ? row['id'] : row.first).to_i ) }
+ connection.execute(select(unique_index_attributes.map(&:to_s).join(',')).to_sql).each do |row|
+ # invalidate the unique index for all attributes
+ unique_index_attributes.each_with_index do |attribute, index|
+ record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]) )
+ end
+ end
end
end
View
24 lib/record_cache/dispatcher.rb
@@ -12,31 +12,31 @@ class Dispatcher
# Roll your own cache strategies by extending from +RecordCache::Strategy::Base+,
# and registering it here +RecordCache::Dispatcher.strategy_classes << MyStrategy+
def self.strategy_classes
- [RecordCache::Strategy::RequestCache, RecordCache::Strategy::IdCache, RecordCache::Strategy::IndexCache]
+ [RecordCache::Strategy::RequestCache, RecordCache::Strategy::UniqueIndexCache, RecordCache::Strategy::FullTableCache, RecordCache::Strategy::IndexCache]
end
def initialize(base)
@base = base
- @strategy_by_id = {}
+ @strategy_by_attribute = {}
end
-
+
# Parse the options provided to the cache_records method and create the appropriate cache strategies.
def parse(options)
# find the record store, possibly based on the :store option
store = record_store(options.delete(:store))
# dispatch the parse call to all known strategies
Dispatcher.strategy_classes.map{ |klass| klass.parse(@base, store, options) }.flatten.compact.each do |strategy|
- raise "Multiple record cache definitions found for '#{strategy.id}' on #{@base.name}" if @strategy_by_id[strategy.id]
+ raise "Multiple record cache definitions found for '#{strategy.attribute}' on #{@base.name}" if @strategy_by_attribute[strategy.attribute]
# and keep track of all strategies
- @strategy_by_id[strategy.id] = strategy
+ @strategy_by_attribute[strategy.attribute] = strategy
end
# make sure the strategies are ordered again on next call to +ordered_strategies+
@ordered_strategies = nil
end
- # Retrieve the caching strategy for the given strategy id
- def [](strategy_id)
- @strategy_by_id[strategy_id]
+ # Retrieve the caching strategy for the given attribute
+ def [](attribute)
+ @strategy_by_attribute[attribute]
end
# Can the cache retrieve the records based on this query?
@@ -62,7 +62,7 @@ def record_change(record, action)
# skip unless something has actually changed
return if action == :update && record.previous_changes.empty?
# dispatch the record change to all known strategies
- @strategy_by_id.values.each { |strategy| strategy.record_change(record, action) }
+ @strategy_by_attribute.values.each { |strategy| strategy.record_change(record, action) }
end
# Explicitly invalidate one or more records
@@ -71,7 +71,7 @@ def record_change(record, action)
def invalidate(strategy, value = nil)
(value = strategy; strategy = :id) unless strategy.is_a?(Symbol)
# call the invalidate method of the chosen strategy
- @strategy_by_id[strategy].invalidate(value) if @strategy_by_id[strategy]
+ @strategy_by_attribute[strategy].invalidate(value) if @strategy_by_attribute[strategy]
# always clear the request cache if invalidate is explicitly called for this class
request_cache.try(:invalidate, value)
end
@@ -101,7 +101,7 @@ def ordered_strategies
@ordered_strategies ||= begin
last_index = Dispatcher.strategy_classes.size
# sort the strategies baed on the +strategy_classes+ index
- ordered = @strategy_by_id.values.sort{ |x,y| Dispatcher.strategy_classes.index(x.class) || last_index <=> Dispatcher.strategy_classes.index(y.class) || last_index }
+ ordered = @strategy_by_attribute.values.sort{ |x,y| Dispatcher.strategy_classes.index(x.class) || last_index <=> Dispatcher.strategy_classes.index(y.class) || last_index }
# and remove the RequestCache from the list
ordered.delete(request_cache) if request_cache
ordered
@@ -111,7 +111,7 @@ def ordered_strategies
# Retrieve the request cache strategy, or
# +nil+ unless the +:request_cache => true+ option was provided.
def request_cache
- @strategy_by_id[:request_cache]
+ @strategy_by_attribute[:request_cache]
end
end
View
44 lib/record_cache/query.rb
@@ -8,7 +8,7 @@ def initialize(equality = nil)
@wheres = equality || {}
@sort_orders = []
@limit = nil
- @where_ids = {}
+ @where_values = {}
end
# Set equality of an attribute (usually found in where clause)
@@ -16,19 +16,23 @@ def where(attribute, values)
@wheres[attribute.to_sym] = values if attribute
end
- # Retrieve the ids (array of positive integers) for the given attribute from the where statements
+ # Retrieve the values for the given attribute from the where statements
# Returns nil if no the attribute is not present
- def where_ids(attribute)
- return @where_ids[attribute] if @where_ids.key?(attribute)
- @where_ids[attribute] ||= array_of_positive_integers(@wheres[attribute])
+ # @param attribute: the attribute name
+ # @param type: the type to be retrieved, :integer or :string (defaults to :integer)
+ def where_values(attribute, type = :integer)
+ return @where_values[attribute] if @where_values.key?(attribute)
+ @where_values[attribute] ||= array_of_values(@wheres[attribute], type)
end
- # Retrieve the single id (positive integer) for the given attribute from the where statements
- # Returns nil if no the attribute is not present, or if it contains an array
- def where_id(attribute)
- ids = where_ids(attribute)
- return nil unless ids && ids.size == 1
- ids.first
+ # Retrieve the single value for the given attribute from the where statements
+ # Returns nil if the attribute is not present, or if it contains multiple values
+ # @param attribute: the attribute name
+ # @param type: the type to be retrieved, :integer or :string (defaults to :integer)
+ def where_value(attribute, type = :integer)
+ values = where_values(attribute, type)
+ return nil unless values && values.size == 1
+ values.first
end
# Add a sort order to the query
@@ -53,8 +57,8 @@ def to_s
s = "SELECT "
s << @wheres.map{|k,v| "#{k} = #{v.inspect}"}.join(" AND ")
if @sort_orders.size > 0
- order_by = @sort_orders.map{|attr,asc| "#{attr} #{asc ? 'ASC' : 'DESC'}"}.join(', ')
- s << " ORDER_BY #{order_by}"
+ order_by_clause = @sort_orders.map{|attr,asc| "#{attr} #{asc ? 'ASC' : 'DESC'}"}.join(', ')
+ s << " ORDER_BY #{order_by_clause}"
end
s << " LIMIT #{@limit}" if @limit
s
@@ -65,18 +69,22 @@ def to_s
def generate_key
key = @wheres.map{|k,v| "#{k}=#{v.inspect}"}.join("&")
if @sort_orders
- order_by = @sort_orders.map{|attr,asc| "#{attr}=#{asc ? 'A' : 'D'}"}.join('-')
- key << ".#{order_by}"
+ order_by_clause = @sort_orders.map{|attr,asc| "#{attr}=#{asc ? 'A' : 'D'}"}.join('-')
+ key << ".#{order_by_clause}"
end
key << "L#{@limit}" if @limit
key
end
- def array_of_positive_integers(values)
+ def array_of_values(values, type)
return nil unless values
values = [values] unless values.is_a?(Array)
- values = values.map{|value| value.to_i} unless values.first.is_a?(Fixnum)
- return nil unless values.all?{ |value| value > 0 } # all values must be positive integers
+ if type == :integer
+ values = values.map{|value| value.to_i} unless values.first.is_a?(Fixnum)
+ return nil unless values.all?{ |value| value > 0 } # all values must be positive integers
+ elsif type == :string
+ values = values.map{|value| value.to_s} unless values.first.is_a?(String)
+ end
values
end
View
10 lib/record_cache/statistics.rb
@@ -32,13 +32,13 @@ def reset!(base = nil)
stats.each{ |s| s.reset! }
end
- # Retrieve the statistics for the given base and strategy_id
- # Returns a hash {<stategy_id> => <statistics} for a model if no strategy is provided
- # Returns a hash of hashes { <model_name> => {<stategy_id> => <statistics} } if no parameter is provided
- def find(base = nil, strategy_id = nil)
+ # Retrieve the statistics for the given base and attribute
+ # Returns a hash {<attribute> => <statistics} for a model if no strategy is provided
+ # Returns a hash of hashes { <model_name> => {<attribute> => <statistics} } if no parameter is provided
+ def find(base = nil, attribute = nil)
stats = (@stats ||= {})
stats = (stats[base.name] ||= {}) if base
- stats = (stats[strategy_id] ||= Counter.new) if strategy_id
+ stats = (stats[attribute] ||= Counter.new) if attribute
stats
end
end
View
16 lib/record_cache/strategy/base.rb
@@ -7,16 +7,17 @@ def self.parse(base, record_store, options)
raise NotImplementedError
end
- def initialize(base, strategy_id, record_store, options)
+ def initialize(base, attribute, record_store, options)
@base = base
- @strategy_id = strategy_id
+ @attribute = attribute
@record_store = record_store
@cache_key_prefix = "rc/#{options[:key] || @base.name}/"
end
- # retrieve the +strategy_id+ for this strategy, usually the column name (unique per model)
- def id
- @strategy_id
+ # Retrieve the +attribute+ for this strategy (unique per model).
+ # May be a non-existing attribute in case a cache is not based on a single attribute.
+ def attribute
+ @attribute
end
# Fetch all records and sort and filter locally
@@ -24,6 +25,7 @@ def fetch(query)
records = fetch_records(query)
Util.filter!(records, query.wheres) if query.wheres.size > 0
Util.sort!(records, query.sort_orders) if query.sorted?
+ records = records[0..query.limit-1] if query.limit
records
end
@@ -62,14 +64,14 @@ def record_store
# find the statistics for this cache strategy
def statistics
- @statistics ||= RecordCache::Statistics.find(@base, @strategy_id)
+ @statistics ||= RecordCache::Statistics.find(@base, @attribute)
end
# retrieve the cache key for the given id, e.g. rc/person/14
def cache_key(id)
"#{@cache_key_prefix}#{id}"
end
-
+
# retrieve the versioned record key, e.g. rc/person/14v1
def versioned_key(cache_key, version)
"#{cache_key}v#{version.to_s}"
View
82 lib/record_cache/strategy/full_table_cache.rb
@@ -0,0 +1,82 @@
+module RecordCache
+ module Strategy
+ class FullTableCache < Base
+ FULL_TABLE = 'full-table'
+
+ # parse the options and return (an array of) instances of this strategy
+ def self.parse(base, record_store, options)
+ return nil unless options[:full_table]
+ FulltableCache.new(base, :full_table, record_store, options)
+ end
+
+ # Can the cache retrieve the records based on this query?
+ def cacheable?(query)
+ true
+ end
+
+ # Clear the cache on any record change
+ def record_change(record, action)
+ version_store.delete(cache_key(FULL_TABLE))
+ end
+
+ # Handle invalidation call
+ def invalidate(id)
+ version_store.delete(cache_key(FULL_TABLE))
+ end
+
+ protected
+
+ # retrieve the record(s) with the given id(s) as an array
+ def fetch_records(query)
+ key = cache_key(FULL_TABLE)
+ # retrieve the current version of the records
+ current_version = version_store.current(key)
+ # get the records from the cache if there is a current version
+ records = current_version ? from_cache(key, current_version) : nil
+ # logging (only in debug mode!) and statistics
+ log_full_table_cache_hit(key, records) if RecordCache::Base.logger.debug?
+ statistics.add(1, records ? 1 : 0) if statistics.active?
+ # no records found?
+ unless records
+ # renew the version in case the version was not known
+ current_version = version_store.renew(key) unless current_version
+ # retrieve all records from the DB
+ records = from_db(key, current_version)
+ end
+ # return the array
+ records
+ end
+
+ private
+
+ # ---------------------------- Querying ------------------------------------
+
+ # retrieve the records from the cache with the given keys
+ def from_cache(key, version)
+ records = record_store.read(versioned_key(key, version))
+ records.map{ |record| Util.deserialize(record) } if records
+ end
+
+ # retrieve the records with the given ids from the database
+ def from_db(key, version)
+ RecordCache::Base.without_record_cache do
+ # retrieve the records from the database
+ records = @base.all.to_a
+ # write all records to the cache
+ record_store.write(versioned_key(key, version), records.map{ |record| Util.serialize(record) })
+ records
+ end
+ end
+
+ # ------------------------- Utility methods ----------------------------
+
+ # log cache hit/miss to debug log
+ def log_full_table_cache_hit(key, records)
+ hit = records ? "hit" : "miss"
+ msg = "FullTableCache #{hit} for model #{@base.name}"
+ RecordCache::Base.logger.debug(msg)
+ end
+
+ end
+ end
+end
View
22 lib/record_cache/strategy/index_cache.rb
@@ -4,7 +4,8 @@ class IndexCache < Base
# parse the options and return (an array of) instances of this strategy
def self.parse(base, record_store, options)
- return nil unless options[:index]
+ return nil unless
+ raise "Index cache '#{options[:index].inspect}' on #{base.name} is redundant as index cache queries are handled by the full table cache." if options[:full_table]
raise ":index => #{options[:index].inspect} option cannot be used unless 'id' is present on #{base.name}" unless base.columns_hash['id']
[options[:index]].flatten.compact.map do |attribute|
type = base.columns_hash[attribute.to_s].try(:type)
@@ -14,26 +15,25 @@ def self.parse(base, record_store, options)
end
end
- def initialize(base, strategy_id, record_store, options)
+ def initialize(base, attribute, record_store, options)
super
- @index = strategy_id
- @index_cache_key_prefix = cache_key(@index) # "/rc/<model>/<index>"
+ @index_cache_key_prefix = cache_key(attribute) # "/rc/<model>/<attribute>"
end
# Can the cache retrieve the records based on this query?
def cacheable?(query)
# allow limit of 1 for has_one
- query.where_id(@index) && (query.limit.nil? || (query.limit == 1 && !query.sorted?))
+ query.where_value(@attribute) && (query.limit.nil? || (query.limit == 1 && !query.sorted?))
end
# Handle create/update/destroy (use record.previous_changes to find the old values in case of an update)
def record_change(record, action)
if action == :destroy
- remove_from_index(record.send(@index), record.id)
+ remove_from_index(record.send(@attribute), record.id)
elsif action == :create
- add_to_index(record.send(@index), record.id)
+ add_to_index(record.send(@attribute), record.id)
else
- index_change = record.previous_changes[@index.to_s]
+ index_change = record.previous_changes[@attribute.to_s]
return unless index_change
remove_from_index(index_change[0], record.id)
add_to_index(index_change[1], record.id)
@@ -49,9 +49,9 @@ def invalidate(value)
# retrieve the record(s) based on the given query
def fetch_records(query)
- value = query.where_id(@index)
+ value = query.where_value(@attribute)
# make sure CacheCase.filter! does not see this where clause anymore
- query.wheres.delete(@index)
+ query.wheres.delete(@attribute)
# retrieve the cache key for this index and value
key = index_cache_key(value)
# retrieve the current version of the ids list
@@ -89,7 +89,7 @@ def fetch_ids_from_cache(versioned_key)
def fetch_ids_from_db(versioned_key, value)
RecordCache::Base.without_record_cache do
# go straight to SQL result for optimal performance
- sql = @base.select('id').where(@index => value).to_sql
+ sql = @base.select('id').where(@attribute => value).to_sql
ids = []; @base.connection.execute(sql).each{ |row| ids << (row.is_a?(Hash) ? row['id'] : row.first).to_i }
record_store.write(versioned_key, ids)
ids
View
53 lib/record_cache/strategy/id_cache.rb → lib/record_cache/strategy/unique_index_cache.rb
@@ -1,23 +1,44 @@
module RecordCache
module Strategy
- class IdCache < Base
+ class UniqueIndexCache < Base
+
+ # All attributes with a unique index for the given model
+ def self.attributes(base)
+ (@attributes ||= {})[base.name] ||= []
+ end
# parse the options and return (an array of) instances of this strategy
def self.parse(base, record_store, options)
- return nil if base.record_cache[:id] # in the end there can be only one +id+ strategy
- return nil unless base.columns_hash['id'] # and there must be an +id+ column in the database
- IdCache.new(base, :id, record_store, options)
+ attributes = [options[:unique_index]].flatten.compact
+ # add unique index for :id by default
+ attributes << :id if base.columns_hash['id'] unless base.record_cache[:id]
+ return nil if attributes.empty?
+ attributes.map do |attribute|
+ type = base.columns_hash[attribute.to_s].try(:type)
+ raise "No column found for unique index '#{index}' on #{base.name}." unless type
+ raise "Incorrect type (expected string or integer, found #{type}) for unique index '#{attribute}' on #{base.name}." unless type == :string || type == :integer
+ UniqueIndexCache.new(base, attribute, record_store, options, type)
+ end
+ end
+
+ def initialize(base, attribute, record_store, options, type)
+ super(base, attribute, record_store, options)
+ # remember the attributes with a unique index
+ UniqueIndexCache.attributes(base) << attribute
+ # for unique indexes that are not on the :id column, use key: rc/<key or model name>/<attribute>:
+ @cache_key_prefix << "#{attribute}:" unless attribute == :id
+ @type = type
end
# Can the cache retrieve the records based on this query?
def cacheable?(query)
- ids = query.where_ids(:id)
- ids && (query.limit.nil? || (query.limit == 1 && ids.size == 1))
+ values = query.where_values(@attribute, @type)
+ values && (query.limit.nil? || (query.limit == 1 && values.size == 1))
end
-
+
# Update the version store and the record store
def record_change(record, action)
- key = cache_key(record.id)
+ key = cache_key(record.send(@attribute))
if action == :destroy
version_store.delete(key)
else
@@ -29,15 +50,15 @@ def record_change(record, action)
# Handle invalidation call
def invalidate(id)
- version_store.delete(cache_key(id))
+ version_store.delete(cache_key(@type == :integer ? id.to_i : id.to_s))
end
protected
# retrieve the record(s) with the given id(s) as an array
def fetch_records(query)
- ids = query.where_ids(:id)
- query.wheres.delete(:id) # make sure CacheCase.filter! does not see this where anymore
+ ids = query.where_values(@attribute, @type)
+ query.wheres.delete(@attribute) # make sure CacheCase.filter! does not see this where anymore
id_to_key_map = ids.inject({}){|h,id| h[id] = cache_key(id); h }
# retrieve the current version of the records
current_versions = version_store.current_multi(id_to_key_map)
@@ -46,7 +67,7 @@ def fetch_records(query)
# retrieve the records from the cache
records = id_to_version_key_map.size > 0 ? from_cache(id_to_version_key_map) : []
# query the records with missing ids
- id_to_key_map.except!(*records.map(&:id))
+ id_to_key_map.except!(*records.map(&@attribute))
# logging (only in debug mode!) and statistics
log_id_cache_hit(ids, id_to_key_map.keys) if RecordCache::Base.logger.debug?
statistics.add(ids.size, records.size) if statistics.active?
@@ -70,12 +91,12 @@ def from_cache(id_to_versioned_key_map)
def from_db(id_to_key_map, id_to_version_key_map)
RecordCache::Base.without_record_cache do
# retrieve the records from the database
- records = @base.where(:id => id_to_key_map.keys).to_a
+ records = @base.where(@attribute => id_to_key_map.keys).to_a
records.each do |record|
- versioned_key = id_to_version_key_map[record.id]
+ versioned_key = id_to_version_key_map[record.send(@attribute)]
unless versioned_key
# renew the key in the version store in case it was missing
- key = id_to_key_map[record.id]
+ key = id_to_key_map[record.send(@attribute)]
versioned_key = versioned_key(key, version_store.renew(key))
end
# store the record based on the versioned key
@@ -91,7 +112,7 @@ def from_db(id_to_key_map, id_to_version_key_map)
def log_id_cache_hit(ids, missing_ids)
hit = missing_ids.empty? ? "hit" : ids.size == missing_ids.size ? "miss" : "partial hit"
missing = missing_ids.empty? || ids.size == missing_ids.size ? "" : ": missing #{missing_ids.inspect}"
- msg = "IdCache #{hit} for ids #{ids.size == 1 ? ids.first : ids.inspect}#{missing}"
+ msg = "UniqueIndexCache on '#{@attribute}' #{hit} for ids #{ids.size == 1 ? ids.first.inspect : ids.inspect}#{missing}"
RecordCache::Base.logger.debug(msg)
end
View
4 spec/lib/dispatcher_spec.rb
@@ -6,7 +6,7 @@
end
it "should return the (ordered) strategy classes" do
- RecordCache::Dispatcher.strategy_classes.should == [RecordCache::Strategy::RequestCache, RecordCache::Strategy::IdCache, RecordCache::Strategy::IndexCache]
+ RecordCache::Dispatcher.strategy_classes.should == [RecordCache::Strategy::RequestCache, RecordCache::Strategy::UniqueIndexCache, RecordCache::Strategy::FullTableCache, RecordCache::Strategy::IndexCache]
end
context "parse" do
@@ -16,7 +16,7 @@
end
it "should return the Cache for the requested strategy" do
- @apple_dispatcher[:id].class.should == RecordCache::Strategy::IdCache
+ @apple_dispatcher[:id].class.should == RecordCache::Strategy::UniqueIndexCache
@apple_dispatcher[:store_id].class.should == RecordCache::Strategy::IndexCache
end
View
32 spec/lib/query_spec.rb
@@ -22,69 +22,69 @@
@query.wheres.should == {:name => "My name", :id => [1, 2, 3], :height => 1.75}
end
- context "where_ids" do
+ context "where_values" do
it "should return nil if the attribute is not defined" do
@query.where(:idx, 15)
- @query.where_ids(:id).should == nil
+ @query.where_values(:id).should == nil
end
it "should return nil if one the value is nil" do
@query.where(:id, nil)
- @query.where_ids(:id).should == nil
+ @query.where_values(:id).should == nil
end
it "should return nil if one of the values is < 1" do
@query.where(:id, [2, 0, 8])
- @query.where_ids(:id).should == nil
+ @query.where_values(:id).should == nil
end
it "should return nil if one of the values is nil" do
@query.where(:id, ["1", nil, "3"])
- @query.where_ids(:id).should == nil
+ @query.where_values(:id).should == nil
end
it "should retrieve an array of integers when a single integer is provided" do
@query.where(:id, 15)
- @query.where_ids(:id).should == [15]
+ @query.where_values(:id).should == [15]
end
it "should retrieve an array of integers when a multiple integers are provided" do
@query.where(:id, [2, 4, 8])
- @query.where_ids(:id).should == [2, 4, 8]
+ @query.where_values(:id).should == [2, 4, 8]
end
it "should retrieve an array of integers when a single string is provided" do
@query.where(:id, "15")
- @query.where_ids(:id).should == [15]
+ @query.where_values(:id).should == [15]
end
it "should retrieve an array of integers when a multiple strings are provided" do
@query.where(:id, ["2", "4", "8"])
- @query.where_ids(:id).should == [2, 4, 8]
+ @query.where_values(:id).should == [2, 4, 8]
end
- it "should cache the array of integers" do
+ it "should cache the array of values" do
@query.where(:id, ["2", "4", "8"])
- ids1 = @query.where_ids(:id)
- ids2 = @query.where_ids(:id)
+ ids1 = @query.where_values(:id)
+ ids2 = @query.where_values(:id)
ids1.object_id.should == ids2.object_id
end
end
- context "where_id" do
+ context "where_value" do
it "should return nil when multiple integers are provided" do
@query.where(:id, [2, 4, 8])
- @query.where_id(:id).should == nil
+ @query.where_value(:id).should == nil
end
it "should return the id when a single integer is provided" do
@query.where(:id, 4)
- @query.where_id(:id).should == 4
+ @query.where_value(:id).should == 4
end
it "should return the id when a single string is provided" do
@query.where(:id, ["4"])
- @query.where_id(:id).should == 4
+ @query.where_value(:id).should == 4
end
end
end
View
10 spec/lib/strategy/id_cache_spec.rb → spec/lib/strategy/unique_index_on_id_cache_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe RecordCache::Strategy::IdCache do
+describe RecordCache::Strategy::UniqueIndexCache do
it "should retrieve an Apple from the cache" do
lambda{ Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
@@ -20,17 +20,17 @@
end
it "should write full hits to the debug log" do
- mock(RecordCache::Base.logger).debug(/IdCache hit for ids 1|^(?!IdCache)/).times(any_times)
+ mock(RecordCache::Base.logger).debug(/UniqueIndexCache on 'id' hit for ids 1|^(?!UniqueIndexCache)/).times(any_times)
Apple.find(1)
end
it "should write full miss to the debug log" do
- mock(RecordCache::Base.logger).debug(/IdCache miss for ids 2|^(?!IdCache)/).times(any_times)
+ mock(RecordCache::Base.logger).debug(/UniqueIndexCache on 'id' miss for ids 2|^(?!UniqueIndexCache)/).times(any_times)
Apple.find(2)
end
it "should write partial hits to the debug log" do
- mock(RecordCache::Base.logger).debug(/IdCache partial hit for ids \[1, 2\]: missing \[2\]|^(?!IdCache)/).times(any_times)
+ mock(RecordCache::Base.logger).debug(/UniqueIndexCache on 'id' partial hit for ids \[1, 2\]: missing \[2\]|^(?!UniqueIndexCache)/).times(any_times)
Apple.where(:id => [1,2]).all
end
end
@@ -169,5 +169,5 @@
@homeless_apple.store_id.should == nil
end
end
-
+
end
View
159 spec/lib/strategy/unique_index_on_string_cache_spec.rb
@@ -0,0 +1,159 @@
+require "spec_helper"
+
+describe RecordCache::Strategy::UniqueIndexCache do
+
+ it "should retrieve an Person from the cache" do
+ lambda{ Person.find_by_name("Fry") }.should miss_cache(Person).on(:name).times(1)
+ lambda{ Person.find_by_name("Fry") }.should hit_cache(Person).on(:name).times(1)
+ end
+
+ it "should retrieve cloned records" do
+ @fry_a = Person.find_by_name("Fry")
+ @fry_b = Person.find_by_name("Fry")
+ @fry_a.should == @fry_b
+ @fry_a.object_id.should_not == @fry_b.object_id
+ end
+
+ context "logging" do
+ before(:each) do
+ Person.find_by_name("Fry")
+ end
+
+ it "should write full hits to the debug log" do
+ mock(RecordCache::Base.logger).debug(/UniqueIndexCache on 'name' hit for ids "Fry"|^(?!UniqueIndexCache)/).times(any_times)
+ Person.find_by_name("Fry")
+ end
+
+ it "should write full miss to the debug log" do
+ mock(RecordCache::Base.logger).debug(/UniqueIndexCache on 'name' miss for ids "Chase"|^(?!UniqueIndexCache)/).times(any_times)
+ Person.find_by_name("Chase")
+ end
+
+ it "should write partial hits to the debug log" do
+ mock(RecordCache::Base.logger).debug(/UniqueIndexCache on 'name' partial hit for ids \["Fry", "Chase"\]: missing \["Chase"\]|^(?!UniqueIndexCache)/).times(any_times)
+ Person.where(:name => ["Fry", "Chase"]).all
+ end
+ end
+
+ context "cacheable?" do
+ before(:each) do
+ # fill cache
+ @fry = Person.find_by_name("Fry")
+ @chase = Person.find_by_name("Chase")
+ end
+
+ # @see https://github.com/orslumen/record-cache/issues/2
+ it "should not use the cache when a lock is used" do
+ lambda{ Person.lock("any_lock").where(:name => "Fry").all }.should_not hit_cache(Person)
+ end
+
+ it "should use the cache when a single id is requested" do
+ lambda{ Person.where(:name => "Fry").all }.should hit_cache(Person).on(:name).times(1)
+ end
+
+ it "should use the cache when a multiple ids are requested" do
+ lambda{ Person.where(:name => ["Fry", "Chase"]).all }.should hit_cache(Person).on(:name).times(2)
+ end
+
+ it "should use the cache when a single id is requested and the limit is 1" do
+ lambda{ Person.where(:name => "Fry").limit(1).all }.should hit_cache(Person).on(:name).times(1)
+ end
+
+ it "should not use the cache when a single id is requested and the limit is > 1" do
+ lambda{ Person.where(:name => "Fry").limit(2).all }.should_not use_cache(Person).on(:name)
+ end
+
+ it "should not use the cache when multiple ids are requested and the limit is 1" do
+ lambda{ Person.where(:name => ["Fry", "Chase"]).limit(1).all }.should_not use_cache(Person).on(:name)
+ end
+
+ it "should use the cache when a single id is requested together with other where clauses" do
+ lambda{ Person.where(:name => "Fry").where(:height => 1.67).all }.should hit_cache(Person).on(:name).times(1)
+ end
+
+ it "should use the cache when a multiple ids are requested together with other where clauses" do
+ lambda{ Person.where(:name => ["Fry", "Chase"]).where(:height => 1.67).all }.should hit_cache(Person).on(:name).times(2)
+ end
+
+ it "should use the cache when a single id is requested together with (simple) sort clauses" do
+ lambda{ Person.where(:name => "Fry").order("name ASC").all }.should hit_cache(Person).on(:name).times(1)
+ end
+
+ it "should use the cache when a multiple ids are requested together with (simple) sort clauses" do
+ lambda{ Person.where(:name => ["Fry", "Chase"]).order("name ASC").all }.should hit_cache(Person).on(:name).times(2)
+ end
+ end
+
+ context "record_change" do
+ before(:each) do
+ # fill cache
+ @fry = Person.find_by_name("Fry")
+ @chase = Person.find_by_name("Chase")
+ end
+
+ it "should invalidate destroyed records" do
+ lambda{ Person.where(:name => "Fry").all }.should hit_cache(Person).on(:name).times(1)
+ @fry.destroy
+ lambda{ @people = Person.where(:name => "Fry").all }.should miss_cache(Person).on(:name).times(1)
+ @people.should == []
+ # try again, to make sure the "missing record" is not cached
+ lambda{ Person.where(:name => "Fry").all }.should miss_cache(Person).on(:name).times(1)
+ end
+
+ it "should add updated records directly to the cache" do
+ @fry.height = 1.71
+ @fry.save!
+ lambda{ @person = Person.find_by_name("Fry") }.should hit_cache(Person).on(:name).times(1)
+ @person.height.should == 1.71
+ end
+
+ it "should add created records directly to the cache" do
+ Person.create!(:name => "Flower", :birthday => Date.civil(1990,07,29), :height => 1.80)
+ lambda{ @person = Person.find_by_name("Flower") }.should hit_cache(Person).on(:name).times(1)
+ @person.height.should == 1.80
+ end
+
+ it "should add updated records to the cache, also when multiple ids are queried" do
+ @fry.height = 1.71
+ @fry.save!
+ lambda{ @people = Person.where(:name => ["Fry", "Chase"]).order("id ASC").all }.should hit_cache(Person).on(:name).times(2)
+ @people.map(&:height).should == [1.71, 1.91]
+ end
+
+ end
+
+ context "invalidate" do
+ before(:each) do
+ @fry = Person.find_by_name("Fry")
+ @chase = Person.find_by_name("Chase")
+ end
+
+ it "should invalidate single records" do
+ Person.record_cache[:name].invalidate("Fry")
+ lambda{ Person.find_by_name("Fry") }.should miss_cache(Person).on(:name).times(1)
+ end
+
+ it "should only miss the cache for the invalidated record when multiple ids are queried" do
+ # miss on 1
+ Person.record_cache[:name].invalidate("Fry")
+ lambda{ Person.where(:name => ["Fry", "Chase"]).all }.should miss_cache(Person).on(:name).times(1)
+ # hit on 2
+ Person.record_cache[:name].invalidate("Fry")
+ lambda{ Person.where(:name => ["Fry", "Chase"]).all }.should hit_cache(Person).on(:name).times(1)
+ # nothing invalidated, both hit
+ lambda{ Person.where(:name => ["Fry", "Chase"]).all }.should hit_cache(Person).on(:name).times(2)
+ end
+
+ it "should invalidate records when using update_all" do
+ Person.where(:id => ["Fry", "Chase", "Penny"]).all # fill id cache on all Adam Store apples
+ lambda{ @people = Person.where(:name => ["Fry", "Chase", "Penny"]).order("name ASC").all }.should hit_cache(Person).on(:name).times(2)
+ @people.map(&:name).should == ["Chase", "Fry", "Penny"]
+ # update 2 of the 3 People
+ Person.where(:name => ["Fry", "Penny"]).update_all(:height => 1.21)
+ lambda{ @people = Person.where(:name => ["Fry", "Chase", "Penny"]).order("height ASC").all }.should hit_cache(Person).on(:name).times(1)
+ @people.map(&:height).should == [1.21, 1.21, 1.91]
+ end
+
+ end
+
+end
View
2  spec/models/person.rb
@@ -1,6 +1,6 @@
class Person < ActiveRecord::Base
- cache_records :store => :shared, :key => "per"
+ cache_records :store => :shared, :key => "per", :unique_index => :name
has_many :apples # cached with index on person_id
has_many :bananas # cached with index on person_id
Please sign in to comment.
Something went wrong with that request. Please try again.