redrecord 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -0
- data/Manifest +7 -0
- data/README +72 -0
- data/Rakefile +11 -0
- data/lib/redrecord.rb +136 -0
- data/redrecord.gemspec +39 -0
- data/test/test_all.rb +107 -0
- data/test/test_helper.rb +68 -0
- metadata +95 -0
data/CHANGELOG
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
v0.1. First version
|
data/Manifest
ADDED
data/README
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
redrecord
|
3
|
+
---------
|
4
|
+
|
5
|
+
This gem pre-caches your ActiveRecord model's calculated attributes (computed
|
6
|
+
fields) in Redis.
|
7
|
+
|
8
|
+
|
9
|
+
Example
|
10
|
+
-------
|
11
|
+
|
12
|
+
class User < ActiveRecord::Base
|
13
|
+
|
14
|
+
cache do
|
15
|
+
def fullname
|
16
|
+
firstname + ' ' + lastname
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
Methods defined inside the "cache" block are redefined to get the answer
|
24
|
+
from redis first. The cached attributes are saved whenever the record is
|
25
|
+
saved, in an after_commit callback.
|
26
|
+
|
27
|
+
|
28
|
+
Cache invalidation for associations
|
29
|
+
-----------------------------------
|
30
|
+
|
31
|
+
Redrecord can be used to cache attributes that use assocations:
|
32
|
+
|
33
|
+
class User < ActiveRecord::Base
|
34
|
+
has_many :preferences
|
35
|
+
|
36
|
+
cache do
|
37
|
+
def preferences_list
|
38
|
+
preferences.map(&:name).join(', ')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
class Preference < ActiveRecord::Base
|
45
|
+
belongs_to :user
|
46
|
+
|
47
|
+
invalidate_cache_on :user
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
In this example, whenever a preference is saved, the associated user record
|
53
|
+
will be recalculated and saved in redis. If it is an array (eg. has_many)
|
54
|
+
then all of the associated records will be re-cached.
|
55
|
+
|
56
|
+
Other instance methods of interest:
|
57
|
+
|
58
|
+
obj.remove_from_cache! # Remove redis cache for an object.
|
59
|
+
obj.add_to_cache! # Recalculate fields and store in redis.
|
60
|
+
obj.cached_fields # hash of the cached fields and their values
|
61
|
+
obj.attribs_with_cached_fields # cached_fields merged with AR attributes
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
Contact the author
|
68
|
+
------------------
|
69
|
+
|
70
|
+
Andrew Snow <andrew@modulus.org>
|
71
|
+
Andys^ on irc.freenode.net
|
72
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'echoe'
|
2
|
+
|
3
|
+
Echoe.new("redrecord") do |p|
|
4
|
+
p.author = "Andrew Snow"
|
5
|
+
p.email = 'andrew@modulus.org'
|
6
|
+
p.summary = "Redis cacheing for ActiveRecord"
|
7
|
+
p.url = "http://github.com/andys/redrecord"
|
8
|
+
p.runtime_dependencies = ['redis']
|
9
|
+
p.development_dependencies = ['active_model', 'active_support']
|
10
|
+
end
|
11
|
+
|
data/lib/redrecord.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
|
2
|
+
class Redrecord
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :redis
|
6
|
+
def update_queue
|
7
|
+
Thread.current[:redrecord_update_queue] ||= []
|
8
|
+
end
|
9
|
+
def is_marshalled?(str)
|
10
|
+
Marshal.dump(nil)[0,2] == str[0,2]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Model
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
def redrecord_cached_fields
|
19
|
+
@redrecord_cached_fields ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def redrecord_invalidation_fields
|
23
|
+
@redrecord_invalidation_fields ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache(*fields, &bl)
|
27
|
+
if block_given?
|
28
|
+
old_methods = instance_methods
|
29
|
+
class_eval(&bl)
|
30
|
+
fields.push(*(instance_methods - old_methods))
|
31
|
+
end
|
32
|
+
redrecord_cached_fields.push(*fields)
|
33
|
+
fields.each do |f|
|
34
|
+
define_method "#{f}_with_cache" do
|
35
|
+
cached_method(f)
|
36
|
+
end
|
37
|
+
alias_method_chain f, :cache
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def invalidate_cache_on(fieldname)
|
42
|
+
redrecord_invalidation_fields << fieldname.to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.included(mod)
|
48
|
+
mod.extend(ClassMethods)
|
49
|
+
mod.send(:after_save, :redrecord_update_queue_save)
|
50
|
+
mod.send(:after_destroy, :redrecord_update_queue_destroy)
|
51
|
+
mod.send(:after_commit, :redrecord_update_queue_commit)
|
52
|
+
mod.send(:after_rollback, :redrecord_update_queue_rollback)
|
53
|
+
end
|
54
|
+
|
55
|
+
def redrecord_update_queue_save
|
56
|
+
Redrecord.update_queue << [:save, self] unless self.class.redrecord_cached_fields.empty?
|
57
|
+
invalidations_for_redrecord_update_queue
|
58
|
+
end
|
59
|
+
|
60
|
+
def invalidations_for_redrecord_update_queue
|
61
|
+
self.class.redrecord_invalidation_fields.each do |f|
|
62
|
+
if((field_value = send(f)).kind_of?(Array))
|
63
|
+
field_value.each {|item| Redrecord.update_queue << [:save, item] }
|
64
|
+
else
|
65
|
+
Redrecord.update_queue << [:save, field_value] if field_value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def redrecord_update_queue_destroy
|
71
|
+
Redrecord.update_queue << [:destroy, self] unless self.class.redrecord_cached_fields.empty?
|
72
|
+
invalidations_for_redrecord_update_queue
|
73
|
+
end
|
74
|
+
|
75
|
+
def redrecord_update_queue_rollback
|
76
|
+
Redrecord.update_queue.clear
|
77
|
+
end
|
78
|
+
|
79
|
+
def redrecord_update_queue_commit
|
80
|
+
Redrecord.update_queue.each do |command, record|
|
81
|
+
if command == :destroy
|
82
|
+
record.remove_from_cache!
|
83
|
+
elsif command == :save
|
84
|
+
record.add_to_cache!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
Redrecord.update_queue.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
def redrecord_key
|
91
|
+
"#{self.class.table_name}:#{self.id}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove_from_cache!
|
95
|
+
Redrecord.redis.del redrecord_key
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_to_cache!
|
99
|
+
Redrecord.redis.hmset(redrecord_key,
|
100
|
+
*(self.class.redrecord_cached_fields.map {|f|
|
101
|
+
val = send("#{f}_without_cache")
|
102
|
+
[f.to_s, String===val && !Redrecord.is_marshalled?(val) ? val : Marshal.dump(val)]
|
103
|
+
}.flatten)
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def cached_method(method_name)
|
108
|
+
redrecord_cached_attrib_hash[method_name.to_sym]
|
109
|
+
end
|
110
|
+
|
111
|
+
def redrecord_redis_cache
|
112
|
+
@redrecord_redis_cache ||= Redrecord.redis.hgetall(redrecord_key)
|
113
|
+
end
|
114
|
+
|
115
|
+
def redrecord_cached_attrib_hash
|
116
|
+
@redrecord_cached_attrib_hash ||= Hash.new do |h,k|
|
117
|
+
h[k.to_sym] = if(cached = (redrecord_redis_cache[k.to_s] unless new_record?))
|
118
|
+
Redrecord.is_marshalled?(cached) ? Marshal.load(cached) : cached
|
119
|
+
else
|
120
|
+
send("#{k}_without_cache")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def attribs_with_cached_fields
|
126
|
+
attributes.merge(cached_fields)
|
127
|
+
end
|
128
|
+
|
129
|
+
def cached_fields
|
130
|
+
self.class.redrecord_cached_fields.inject({}) {|hsh,field| hsh[field] = send(field) ; hsh }
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
data/redrecord.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "redrecord"
|
5
|
+
s.version = "0.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Andrew Snow"]
|
9
|
+
s.date = "2012-01-23"
|
10
|
+
s.description = "Redis cacheing for ActiveRecord"
|
11
|
+
s.email = "andrew@modulus.org"
|
12
|
+
s.extra_rdoc_files = ["CHANGELOG", "README", "lib/redrecord.rb"]
|
13
|
+
s.files = ["CHANGELOG", "README", "Rakefile", "lib/redrecord.rb", "test/test_all.rb", "test/test_helper.rb", "Manifest", "redrecord.gemspec"]
|
14
|
+
s.homepage = "http://github.com/andys/redrecord"
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Redrecord", "--main", "README"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = "redrecord"
|
18
|
+
s.rubygems_version = "1.8.10"
|
19
|
+
s.summary = "Redis cacheing for ActiveRecord"
|
20
|
+
s.test_files = ["test/test_all.rb"]
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency(%q<redis>, [">= 0"])
|
27
|
+
s.add_development_dependency(%q<active_model>, [">= 0"])
|
28
|
+
s.add_development_dependency(%q<active_support>, [">= 0"])
|
29
|
+
else
|
30
|
+
s.add_dependency(%q<redis>, [">= 0"])
|
31
|
+
s.add_dependency(%q<active_model>, [">= 0"])
|
32
|
+
s.add_dependency(%q<active_support>, [">= 0"])
|
33
|
+
end
|
34
|
+
else
|
35
|
+
s.add_dependency(%q<redis>, [">= 0"])
|
36
|
+
s.add_dependency(%q<active_model>, [">= 0"])
|
37
|
+
s.add_dependency(%q<active_support>, [">= 0"])
|
38
|
+
end
|
39
|
+
end
|
data/test/test_all.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_support/core_ext/module/aliasing'
|
3
|
+
require "#{File.dirname(__FILE__)}/../lib/redrecord"
|
4
|
+
require 'test/unit'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'redis'
|
7
|
+
require "#{File.dirname(__FILE__)}/test_helper"
|
8
|
+
|
9
|
+
class TestRedrecord < Test::Unit::TestCase
|
10
|
+
def setup
|
11
|
+
$redis.flushdb
|
12
|
+
@user = TestUser.new(1, 'John', 'Smith')
|
13
|
+
$saved = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_cached_string_attribute_save
|
17
|
+
# start with a blank slate and save the record
|
18
|
+
assert_equal({}, $redis.hgetall('TestUser:1'))
|
19
|
+
assert_nil @user.recalculated
|
20
|
+
@user.save
|
21
|
+
|
22
|
+
# It should now be saved in redis
|
23
|
+
assert @user.recalculated
|
24
|
+
assert_equal 'John Smith', $redis.hget('TestUser:1', 'full_name')
|
25
|
+
|
26
|
+
# different object should get the value straight of redis
|
27
|
+
u2 = TestUser.new(1)
|
28
|
+
assert_equal 'John Smith', u2.full_name
|
29
|
+
assert_nil u2.recalculated
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def test_cached_nil_attribute_save
|
34
|
+
@user.save
|
35
|
+
assert_equal Marshal.dump(nil), $redis.hget('TestUser:1', 'nil')
|
36
|
+
u2 = TestUser.new(1, 'John', 'Smith')
|
37
|
+
assert_nil u2.nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_invalidation_on_save
|
41
|
+
@user.save
|
42
|
+
@user.first_name = 'Bob'
|
43
|
+
@user.save
|
44
|
+
|
45
|
+
assert_equal 'Bob Smith', $redis.hget('TestUser:1', 'full_name')
|
46
|
+
|
47
|
+
u2 = TestUser.new(1)
|
48
|
+
assert_equal 'Bob Smith', u2.full_name
|
49
|
+
assert_nil u2.recalculated
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_invalidation_on_delete
|
53
|
+
@user.save
|
54
|
+
assert_equal 'John Smith', $redis.hget('TestUser:1', 'full_name')
|
55
|
+
@user.destroy
|
56
|
+
assert_equal({}, $redis.hgetall('TestUser:1'))
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_invalidation_on_rollback
|
60
|
+
@user.save_with_rollback
|
61
|
+
assert_equal({}, $redis.hgetall('TestUser:1'))
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_invalidation_on_association_create
|
65
|
+
# Create two groups which are attached to the user
|
66
|
+
TestGroup.new(1, 'users', @user).save
|
67
|
+
TestGroup.new(2, 'admins', @user).save
|
68
|
+
#@user.save
|
69
|
+
|
70
|
+
# Now a fresh user record should get the answer out of cache
|
71
|
+
u2 = TestUser.new(1)
|
72
|
+
assert_equal ['admins', 'users'], u2.group_names
|
73
|
+
assert_nil u2.recalculated
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_invalidation_on_association
|
77
|
+
# Create two groups which are attached to the user
|
78
|
+
TestGroup.new(1, 'users', @user).save
|
79
|
+
g = TestGroup.new(2, 'admins', @user).save
|
80
|
+
@user.save
|
81
|
+
|
82
|
+
# Removing a group should also refresh the cache
|
83
|
+
g.destroy
|
84
|
+
assert_equal ['users'], TestUser.new(1).group_names
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_attribs
|
88
|
+
assert_equal({
|
89
|
+
:nil => nil,
|
90
|
+
:group_names => [],
|
91
|
+
:full_name => 'John Smith'},
|
92
|
+
@user.cached_fields)
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_cached_fields
|
96
|
+
assert_equal({
|
97
|
+
:first_name => 'John',
|
98
|
+
:last_name => 'Smith',
|
99
|
+
:id => 1,
|
100
|
+
:nil => nil,
|
101
|
+
:group_names => [],
|
102
|
+
:full_name => 'John Smith'},
|
103
|
+
@user.attribs_with_cached_fields)
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
$redis = Redis.new(:host => 'localhost', :port => 6379) # TODO: dangerous!
|
2
|
+
Redrecord.redis = $redis
|
3
|
+
|
4
|
+
class TestModel
|
5
|
+
extend ActiveModel::Callbacks
|
6
|
+
|
7
|
+
define_model_callbacks :save, :destroy, :commit, :rollback
|
8
|
+
|
9
|
+
include Redrecord::Model
|
10
|
+
|
11
|
+
def save
|
12
|
+
run_callbacks :commit do
|
13
|
+
run_callbacks :save do
|
14
|
+
$saved[redrecord_key] = self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
def destroy
|
19
|
+
run_callbacks :commit do
|
20
|
+
run_callbacks :destroy do
|
21
|
+
$saved.delete(redrecord_key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def save_with_rollback
|
26
|
+
run_callbacks :rollback do
|
27
|
+
run_callbacks :save do
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def new_record?
|
32
|
+
!$saved[redrecord_key]
|
33
|
+
end
|
34
|
+
def self.table_name
|
35
|
+
self.to_s.split('::').last
|
36
|
+
end
|
37
|
+
def attributes
|
38
|
+
instance_variables.map {|var| var.to_s.gsub(/^@/,'').to_sym }.inject({}) {|hsh,var| hsh[var] = send(var) ; hsh}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class TestGroup < TestModel
|
43
|
+
attr_accessor :id, :name, :user
|
44
|
+
def initialize(id, name=nil, user=nil)
|
45
|
+
@id, @name, @user = id, name, user
|
46
|
+
end
|
47
|
+
invalidate_cache_on :user
|
48
|
+
end
|
49
|
+
|
50
|
+
class TestUser < TestModel
|
51
|
+
attr_accessor :first_name, :last_name, :id, :recalculated
|
52
|
+
def initialize(id, first_name=nil, last_name=nil)
|
53
|
+
@id, @first_name, @last_name = id, first_name, last_name
|
54
|
+
end
|
55
|
+
|
56
|
+
cache do
|
57
|
+
def full_name
|
58
|
+
@recalculated = true
|
59
|
+
"#{first_name} #{last_name}"
|
60
|
+
end
|
61
|
+
def nil
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
def group_names
|
65
|
+
$saved.values.select {|v| TestGroup===v && v.user.id == self.id }.map(&:name).sort
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redrecord
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Snow
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &71893080 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *71893080
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: active_model
|
27
|
+
requirement: &71892570 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *71892570
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: active_support
|
38
|
+
requirement: &71891930 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *71891930
|
47
|
+
description: Redis cacheing for ActiveRecord
|
48
|
+
email: andrew@modulus.org
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files:
|
52
|
+
- CHANGELOG
|
53
|
+
- README
|
54
|
+
- lib/redrecord.rb
|
55
|
+
files:
|
56
|
+
- CHANGELOG
|
57
|
+
- README
|
58
|
+
- Rakefile
|
59
|
+
- lib/redrecord.rb
|
60
|
+
- test/test_all.rb
|
61
|
+
- test/test_helper.rb
|
62
|
+
- Manifest
|
63
|
+
- redrecord.gemspec
|
64
|
+
homepage: http://github.com/andys/redrecord
|
65
|
+
licenses: []
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --line-numbers
|
69
|
+
- --inline-source
|
70
|
+
- --title
|
71
|
+
- Redrecord
|
72
|
+
- --main
|
73
|
+
- README
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '1.2'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project: redrecord
|
90
|
+
rubygems_version: 1.8.10
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Redis cacheing for ActiveRecord
|
94
|
+
test_files:
|
95
|
+
- test/test_all.rb
|