Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,29 @@ reduce your database load.

== Usage:

require 'record_cache'

class User < ActiveRecord
class Foo < ActiveRecord
record_cache :by => :id
record_cache :id, :by => :username
record_cache :id, :by => :owner_id
end

# These will use the cache now.
User.find(1)
User.find_by_id(2)
User.find_all_by_username('chuck')
Foo.find(1)
Foo.find_by_id(2)
Foo.find_all_by_owner_id(3)

Invalidation is handled for you using callbacks. Be careful though if you modify records
directly using SQL. Both update_all and delete_all handle invalidations for you, but other
direct SQL will not.
Invalidation is handled for you using callbacks.

== Install:

gem install record_cache

== Dependencies:
First, install the after_commit plugin from: http://github.com/ninjudd/after_commit

* {after_commit}[http://github.com/freelancing-god/after_commit]
* {memcache}[http://github.com/ninjudd/memcache]
Then install the following gems:

RecordCache is confirmed to work with Rails 2.3.9. It does not currently work with Rails 3.
sudo gem install ninjudd-deferrable -s http://gems.github.com
sudo gem install ninjudd-ordered_set -s http://gems.github.com
sudo gem install ninjudd-memcache -s http://gems.github.com
sudo gem install ninjudd-cache_version -s http://gems.github.com
sudo gem install ninjudd-record_cache -s http://gems.github.com

== License:

Expand Down
6 changes: 0 additions & 6 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ begin
s.homepage = "http://github.com/ninjudd/record_cache"
s.description = "Active Record caching and indexing in memcache"
s.authors = ["Justin Balthrop"]
s.add_dependency('after_commit', '>= 1.0.0')
s.add_dependency('deferrable', '>= 0.1.0')
s.add_dependency('memcache', '>= 1.0.0')
s.add_dependency('cache_version', '>= 0.9.4')
s.add_dependency('activerecord', '>= 2.0.0')
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
end
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.12
3.0.0
4 changes: 4 additions & 0 deletions VERSION.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
:minor: 9
:patch: 6
:major: 0
159 changes: 99 additions & 60 deletions lib/record_cache.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'rubygems'
require 'memcache'
require 'active_record'
require 'ordered_set'
require 'cache_version'
require 'deferrable'

Expand All @@ -17,13 +18,18 @@ def self.config(opts = nil)
@config ||= {}
end
end

def self.with_config(opts)
old, @config = @config, @config.merge(opts)
yield
ensure
@config = old
end


def self.db(model_class)
db = model_class.connection

# Always use the master connection since we are caching.
@has_data_fabric ||= defined?(DataFabric::ConnectionProxy)
if @has_data_fabric and db.kind_of?(DataFabric::ConnectionProxy)
db = model_class.connection
if false && defined?(DataFabric::ConnectionProxy) and db.kind_of?(DataFabric::ConnectionProxy)
model_class.record_cache_config[:use_slave] ? db.send(:connection) : db.send(:master)
else
db
Expand All @@ -33,12 +39,13 @@ def self.db(model_class)
module InstanceMethods
def invalidate_record_cache
self.class.each_cached_index do |index|
index.invalidate_model(self)
index.invalidate_model(self)
index.clear_deferred
end
end

def invalidate_record_cache_deferred
return unless (self.changed? || self.destroyed?)
self.class.each_cached_index do |index|
# Have to invalidate both before and after commit.
index.invalidate_model(self)
Expand All @@ -53,41 +60,26 @@ def complete_deferred_record_cache_invalidations

def attr_was(attr)
attr = attr.to_s
if ['id', 'type'].include?(attr) or not attribute_changed?(attr)
read_attribute(attr)
else
changed_attributes[attr]
end
['id', 'type'].include?(attr) ? send(attr) : send(:attribute_was, attr)
end
end

module ClassMethods
module ClassMethods
def find_with_caching(*args, &block)
if args.last.is_a?(Hash)
args.last.delete_if {|k,v| v.nil?}
args.pop if args.last.empty?
end

if [:all, :first, :last].include?(args.first)
opts = args.last
if opts.is_a?(Hash) and opts.keys == [:conditions]
# Try to match the SQL.
if opts[:conditions].kind_of?(Hash)
field = nil
value = nil
if opts[:conditions].keys.size == 1
opts[:conditions].each {|f,v| field, value = f,v}
end
elsif opts[:conditions] =~ /^(?:"?#{table_name}"?\.)?"?(\w+)"? = (?:(\d+)|'(\w+)')$/i
field, value = $1, ($3 || $2)
elsif opts[:conditions] =~ /^(?:"?#{table_name}"?\.)?"?(\w+)"? IN \(([\d,]*)\)$/i
field, value = $1, $2
value = value.split(',')
end

if field and value
if opts[:conditions] =~ /^"?#{table_name}"?."?(\w+)"? = (?:(\d+)|'(\w+)')$/
field = $1
value = ($3 || $2)
index = cached_index("by_#{field}")
return index.find_by_field([value].flatten, self, args.first) if index
return index.find_by_field([value], self, args.first) if index
end
end
elsif not args.last.is_a?(Hash)
Expand All @@ -99,6 +91,43 @@ def find_with_caching(*args, &block)
find_without_caching(*args, &block)
end

# Match sql with regex, locate the index and query cache/db
def match_and_find_by_field(sql, regex, type)
match_data = regex.match(sql)
if match_data
field = match_data[1]
value = (match_data[3] || match_data[2])
index = cached_index("by_#{field}")
if index
records = index.find_by_field([value], self, type)
# we must return an array
if records.is_a?(Array)
return records
end
return records ? [records] : []
end
end
end
private :match_and_find_by_field

# ActiveRecord ends up making a call to find_by_sql in the case of associations
# for example in a call to Company.first.employees
def find_by_sql_with_caching(*args, &block)
if args.is_a?(Array) and args.size==1
regex_nolimit = /^SELECT\s+\S+\.\*\s+FROM\s+\S+\s+WHERE\s+\(?"?#{table_name}"?."?(\w+)"?\s+=\s+(?:(\d+)|'([^']+)')\)?$/
regex_withlimit = /^SELECT\s+\S+\.\*\s+FROM\s+\S+\s+WHERE\s+\(?"?#{table_name}"?."?(\w+)"?\s+=\s+(?:(\d+)|'([^']+)')\)?\s+LIMIT\s+1$/
if records = match_and_find_by_field(args.first, regex_nolimit, :all)
return records
elsif records = match_and_find_by_field(args.first, regex_withlimit, :first)
return records
end
end

# All other queries are sent back to ActiveRecord
find_by_sql_without_caching(*args, &block)
end


def update_all_with_invalidate(updates, conditions = nil)
invalidate_from_conditions(conditions, :update) do |conditions|
update_all_without_invalidate(updates, conditions)
Expand All @@ -111,6 +140,21 @@ def delete_all_with_invalidate(conditions = nil)
end
end

def delete_with_invalidate(arg)
if arg.is_a? Fixnum
condition = { :id => arg }
elsif arg.is_a? ActiveRecord::Base
condition = { :id => arg.id }
elsif arg.is_a? Array
condition = { :id => arg.map(&:id) }
else
raise "Unexpected type for RecordCache delete_with_invalidate arg=#{arg.inspect}"
end
invalidate_from_conditions(condition) do |cond|
delete_without_invalidate(arg)
end
end

def invalidate_from_conditions(conditions, flag = nil)
if conditions.nil?
# Just invalidate all indexes.
Expand All @@ -120,8 +164,7 @@ def invalidate_from_conditions(conditions, flag = nil)
end

# Freeze ids to avoid race conditions.
sql = "SELECT id FROM #{table_name} "
self.send(:add_conditions!, sql, conditions, self.send(:scope, :find))
sql = self.select(:id).where(conditions).to_sql
ids = RecordCache.db(self).select_values(sql)

return if ids.empty?
Expand All @@ -137,9 +180,9 @@ def invalidate_from_conditions(conditions, flag = nil)
result = yield(conditions)

# Finish invalidating with prior attributes.
lambdas.each {|l| l.call}
lambdas.each {|l| l.call}
end

# Invalidate again afterwards if we are updating (or for the first time if no block was given).
if flag == :update or not block_given?
each_cached_index do |index|
Expand Down Expand Up @@ -175,7 +218,7 @@ def add_cached_index(index)
def each_cached_index
cached_index_names.each do |index_name|
yield cached_index(index_name)
end
end
end

def cached_index_names
Expand All @@ -185,11 +228,16 @@ def cached_index_names
end

def record_cache_config(opts = nil)
if opts
record_cache_config.merge!(opts)
else
@record_cache_config ||= RecordCache.config.clone
end
@record_cache_config ||= {}
@record_cache_config.merge!(opts) if opts
RecordCache.config.merge(@record_cache_config)
end

def with_record_cache_config(opts)
old, @record_cache_config = @record_cache_config, @record_cache_config.merge(opts)
yield
ensure
@record_cache_config = old
end
end

Expand All @@ -216,11 +264,7 @@ def record_cache(*args)
[:first, :all, :set, :raw, :ids].each do |type|
next if type == :ids and index.name == 'by_id'
define_method( index.find_method_name(type) ) do |keys|
if self.send(:scope,:find) and self.send(:scope,:find).any?
self.method_missing(index.find_method_name(type), keys)
else
index.find_by_field(keys, self, type)
end
index.find_by_field(keys, self, type)
end
end
end
Expand All @@ -231,7 +275,7 @@ def record_cache(*args)
define_method( "all_#{index.name.pluralize}_by_#{index.index_field}" ) do |keys|
index.field_lookup(keys, self, field, :all)
end

define_method( "#{index.name.pluralize}_by_#{index.index_field}" ) do |keys|
index.field_lookup(keys, self, field)
end
Expand All @@ -240,8 +284,9 @@ def record_cache(*args)
index.field_lookup(keys, self, field, :first)
end
end

if index.auto_name?

(field_lookup + index.fields).each do |field|
next if field == index.index_field
plural_field = field.pluralize
Expand All @@ -255,20 +300,22 @@ def record_cache(*args)
define_method( "#{prefix}#{plural_field}_by_#{index.index_field}" ) do |keys|
index.field_lookup(keys, self, field)
end

define_method( "#{prefix}#{field}_by_#{index.index_field}" ) do |keys|
index.field_lookup(keys, self, field, :first)
end
end
end

if first_index
alias_method_chain :find, :caching
alias_method_chain :update_all, :invalidate
alias_method_chain :delete_all, :invalidate
alias_method_chain :find, :caching
alias_method_chain :find_by_sql, :caching
alias_method_chain :update_all, :invalidate
alias_method_chain :delete_all, :invalidate
alias_method_chain :delete, :invalidate
end
end

if first_index
after_save :invalidate_record_cache_deferred
after_destroy :invalidate_record_cache_deferred
Expand All @@ -280,11 +327,3 @@ def record_cache(*args)
end

ActiveRecord::Base.send(:extend, RecordCache::ActiveRecordExtension)

unless defined?(PGconn) and PGconn.respond_to?(:quote_ident)
class PGconn
def self.quote_ident(name)
%("#{name}")
end
end
end
Loading