redrecord 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|