cached_record 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +13 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +391 -0
- data/Rakefile +90 -0
- data/VERSION +1 -0
- data/benchmark/setup.rb +77 -0
- data/cached_record.gemspec +26 -0
- data/config/database.yml +17 -0
- data/db/cached_record.sql +71 -0
- data/lib/cached_record/cache.rb +125 -0
- data/lib/cached_record/orm/active_record.rb +76 -0
- data/lib/cached_record/orm/data_mapper.rb +87 -0
- data/lib/cached_record/orm.rb +188 -0
- data/lib/cached_record/version.rb +7 -0
- data/lib/cached_record.rb +22 -0
- data/lib/gem_ext/redis.rb +25 -0
- data/log/.gitkeep +0 -0
- data/script/console +8 -0
- data/script/setup.rb +37 -0
- data/test/gem_ext/test_redis.rb +35 -0
- data/test/test_helper/coverage.rb +8 -0
- data/test/test_helper/db.rb +14 -0
- data/test/test_helper/minitest.rb +24 -0
- data/test/test_helper/setup.rb +16 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/orm/test_active_record.rb +628 -0
- data/test/unit/orm/test_data_mapper.rb +616 -0
- data/test/unit/test_cache.rb +351 -0
- data/test/unit/test_cached_record.rb +34 -0
- data/test/unit/test_orm.rb +193 -0
- metadata +201 -0
data/config/database.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
development:
|
2
|
+
adapter: mysql2
|
3
|
+
host: localhost
|
4
|
+
database: cached_record_development
|
5
|
+
username: root
|
6
|
+
password:
|
7
|
+
pool: 5
|
8
|
+
timeout: 5000
|
9
|
+
|
10
|
+
test:
|
11
|
+
adapter: mysql2
|
12
|
+
host: localhost
|
13
|
+
database: cached_record_test
|
14
|
+
username: root
|
15
|
+
password:
|
16
|
+
pool: 5
|
17
|
+
timeout: 5000
|
@@ -0,0 +1,71 @@
|
|
1
|
+
DROP TABLE IF EXISTS `articles`;
|
2
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
3
|
+
/*!40101 SET character_set_client = utf8 */;
|
4
|
+
CREATE TABLE `articles` (
|
5
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
6
|
+
`title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
7
|
+
`content` text COLLATE utf8_unicode_ci,
|
8
|
+
`author_id` int(11) DEFAULT NULL,
|
9
|
+
`foo_id` int(11) DEFAULT NULL,
|
10
|
+
`published_at` datetime DEFAULT NULL,
|
11
|
+
`created_at` datetime NOT NULL,
|
12
|
+
`updated_at` datetime NOT NULL,
|
13
|
+
PRIMARY KEY (`id`),
|
14
|
+
KEY `author_id` (`author_id`)
|
15
|
+
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
16
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
17
|
+
INSERT INTO `articles` VALUES (1,'Behold! It\'s CachedRecord!','Cache ORM instances to avoid database queries',1,2,'2013-08-01 12:00:00','2013-08-01 10:00:00','2013-08-01 11:00:00');
|
18
|
+
DROP TABLE IF EXISTS `articles_tags`;
|
19
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
20
|
+
/*!40101 SET character_set_client = utf8 */;
|
21
|
+
CREATE TABLE `articles_tags` (
|
22
|
+
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
23
|
+
`article_id` int(11) DEFAULT NULL,
|
24
|
+
`tag_id` int(11) DEFAULT NULL,
|
25
|
+
PRIMARY KEY (`id`),
|
26
|
+
KEY `articles_tags` (`article_id`,`tag_id`)
|
27
|
+
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
28
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
29
|
+
INSERT INTO `articles_tags` VALUES (1,1,1),(2,1,2);
|
30
|
+
DROP TABLE IF EXISTS `comments`;
|
31
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
32
|
+
/*!40101 SET character_set_client = utf8 */;
|
33
|
+
CREATE TABLE `comments` (
|
34
|
+
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
35
|
+
`content` text COLLATE utf8_unicode_ci,
|
36
|
+
`article_id` int(11) DEFAULT NULL,
|
37
|
+
`poster_id` int(11) DEFAULT NULL,
|
38
|
+
`created_at` datetime DEFAULT NULL,
|
39
|
+
`updated_at` datetime DEFAULT NULL,
|
40
|
+
PRIMARY KEY (`id`),
|
41
|
+
KEY `article_id` (`article_id`),
|
42
|
+
KEY `poster_id` (`poster_id`)
|
43
|
+
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
44
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
45
|
+
INSERT INTO `comments` VALUES (1,'What a great article! :)',1,2,'2013-08-02 12:00:00','2013-08-02 13:00:00'),(2,'Thanks!',1,1,'2013-08-02 14:00:00','2013-08-02 14:00:00');
|
46
|
+
DROP TABLE IF EXISTS `tags`;
|
47
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
48
|
+
/*!40101 SET character_set_client = utf8 */;
|
49
|
+
CREATE TABLE `tags` (
|
50
|
+
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
51
|
+
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
52
|
+
`created_at` datetime DEFAULT NULL,
|
53
|
+
`updated_at` datetime DEFAULT NULL,
|
54
|
+
PRIMARY KEY (`id`)
|
55
|
+
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
56
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
57
|
+
INSERT INTO `tags` VALUES (1,'ruby','2013-08-01 08:00:00','2013-08-01 08:00:00'),(2,'gem','2013-08-01 08:01:00','2013-08-01 08:01:00');
|
58
|
+
DROP TABLE IF EXISTS `users`;
|
59
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
60
|
+
/*!40101 SET character_set_client = utf8 */;
|
61
|
+
CREATE TABLE `users` (
|
62
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
63
|
+
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
64
|
+
`description` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
65
|
+
`active` tinyint(1) DEFAULT NULL,
|
66
|
+
`created_at` datetime NOT NULL,
|
67
|
+
`updated_at` datetime NOT NULL,
|
68
|
+
PRIMARY KEY (`id`)
|
69
|
+
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
70
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
71
|
+
INSERT INTO `users` VALUES (1,'Paul Engel','Author of CachedRecord',1,'2013-08-01 11:00:00','2013-08-01 12:00:00'),(2,'Ken Adams','Some guy',1,'2013-08-02 10:00:00','2013-08-02 11:00:00');
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "dalli"
|
2
|
+
require "redis"
|
3
|
+
|
4
|
+
module CachedRecord
|
5
|
+
module Cache
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
def self.setup(store, options = {})
|
9
|
+
if valid_store? store
|
10
|
+
send store, options
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.memcached(options = nil)
|
15
|
+
if stores[:memcached].nil? || options
|
16
|
+
options ||= {}
|
17
|
+
host = options.delete(:host) || "localhost"
|
18
|
+
port = options.delete(:port) || 11211
|
19
|
+
stores[:memcached] = Dalli::Client.new "#{host}:#{port}", options
|
20
|
+
end
|
21
|
+
stores[:memcached]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.redis(options = nil)
|
25
|
+
if stores[:redis].nil? || options
|
26
|
+
options ||= {}
|
27
|
+
stores[:redis] = Redis.new options
|
28
|
+
end
|
29
|
+
stores[:redis]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.cache
|
33
|
+
@cache ||= {Dalli::Client => {}, Redis => {}}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.clear!
|
37
|
+
@cache = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.get(klass, id)
|
41
|
+
retained(klass, id) || begin
|
42
|
+
cache_string = store(klass).get(klass.cache_key(id)) || begin
|
43
|
+
return unless (instance = yield if block_given?)
|
44
|
+
set instance
|
45
|
+
end
|
46
|
+
json, epoch_time = split_cache_string(cache_string)
|
47
|
+
memoized(klass, id, epoch_time) do
|
48
|
+
klass.load_cache_json JSON.parse(json)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.set(instance)
|
54
|
+
"#{instance.to_cache_json}#{"@#{Time.now.to_i}" if instance.class.as_cache[:memoize]}".tap do |cache_string|
|
55
|
+
store(instance.class).set instance.class.cache_key(instance.id), cache_string, instance.class.as_cache[:expire]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.expire(instance)
|
60
|
+
klass = instance.class
|
61
|
+
cache_key = klass.cache_key instance.id
|
62
|
+
store(klass).delete cache_key
|
63
|
+
cache[store(klass).class].delete cache_key if klass.as_cache[:memoize]
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.retained(klass, id)
|
68
|
+
return unless klass.as_cache[:memoize] && klass.as_cache[:retain]
|
69
|
+
cache_hash, cache_key = cache[store(klass).class], klass.cache_key(id)
|
70
|
+
|
71
|
+
if (cache_entry = cache_hash[cache_key]) && (Time.now.to_i < cache_entry[:cache_hit])
|
72
|
+
cache_entry[:instance]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.memoized(klass, id, epoch_time)
|
77
|
+
return yield unless klass.as_cache[:memoize]
|
78
|
+
cache_hash, cache_key = cache[store(klass).class], klass.cache_key(id)
|
79
|
+
cache_hit = klass.as_cache[:retain] ? {:cache_hit => (Time.now + klass.as_cache[:retain]).to_i} : {}
|
80
|
+
|
81
|
+
if (cache_entry = cache_hash[cache_key]) && (epoch_time == cache_entry[:epoch_time])
|
82
|
+
cache_entry.merge! cache_hit
|
83
|
+
cache_entry[:instance]
|
84
|
+
else
|
85
|
+
yield.tap do |instance|
|
86
|
+
cache_hash[cache_key] = {:instance => instance, :epoch_time => epoch_time}.merge(cache_hit) if instance
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def self.valid_store?(arg)
|
94
|
+
[:memcached, :redis].include?(arg.to_sym)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.stores
|
98
|
+
@stores ||= {}
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.store(klass)
|
102
|
+
store = klass.as_cache[:store] || begin
|
103
|
+
if stores.size == 1
|
104
|
+
stores.keys.first
|
105
|
+
else
|
106
|
+
raise Error, "Cannot determine default cache store (store size is not 1: #{@stores.keys.sort.inspect})"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
if valid_store?(store)
|
110
|
+
send(store)
|
111
|
+
else
|
112
|
+
raise Error, "Invalid cache store :#{store} passed"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.split_cache_string(string)
|
117
|
+
reg_exp = /@(\d+)$/
|
118
|
+
string.match(reg_exp)
|
119
|
+
json = string.gsub(reg_exp, "")
|
120
|
+
epoch_time = $1.to_i if $1
|
121
|
+
[json, epoch_time]
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module CachedRecord
|
2
|
+
module ORM
|
3
|
+
module ActiveRecord
|
4
|
+
|
5
|
+
def self.setup?
|
6
|
+
!!defined?(::ActiveRecord)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.setup
|
10
|
+
return false unless setup?
|
11
|
+
::ActiveRecord::Base.send :include, ORM
|
12
|
+
::ActiveRecord::Base.extend ClassMethods
|
13
|
+
::ActiveRecord::Base.send :include, InstanceMethods
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def uncached(id)
|
19
|
+
find(id)
|
20
|
+
end
|
21
|
+
def new_cached_instance(attributes, foreign_keys, variables)
|
22
|
+
super.tap do |instance|
|
23
|
+
instance.instance_variable_set :@new_record, false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
private
|
27
|
+
def set_cached_association(instance, key, value)
|
28
|
+
return unless value
|
29
|
+
if reflection = _cache_reflection_(instance, key, value)
|
30
|
+
value = _cache_value_(instance, reflection, value)
|
31
|
+
instance.send :"#{reflection.name}=", value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
def _cache_reflection_(instance, key, value)
|
35
|
+
if key.to_s.match /^_(.*)_ids?/
|
36
|
+
reflections[$1.pluralize.to_sym]
|
37
|
+
else
|
38
|
+
instance.send :"#{key}=", value
|
39
|
+
reflections.detect{|k, v| v.foreign_key == key.to_s}.try(:last)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
def _cache_value_(instance, reflection, value)
|
43
|
+
if value.is_a? Array
|
44
|
+
(instance.respond_to?(:association) ? instance.association(reflection.name) : instance.send(reflection.name).instance_variable_get(:@association)).loaded!
|
45
|
+
value.collect{|x| reflection.klass.cached x}
|
46
|
+
else
|
47
|
+
reflection.klass.cached value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module InstanceMethods
|
53
|
+
def cache_attributes
|
54
|
+
as_json(cache_json_options.slice(:only).merge(:root => false)).symbolize_keys!.merge cache_foreign_keys
|
55
|
+
end
|
56
|
+
def cache_foreign_keys
|
57
|
+
(cache_json_options[:include] || {}).inject({}) do |json, name|
|
58
|
+
reflection = self.class.reflections[name]
|
59
|
+
json.merge cache_foreign_key(name, reflection, send(name))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
def cache_foreign_key(name, reflection, value)
|
63
|
+
case reflection.macro
|
64
|
+
when :belongs_to
|
65
|
+
{:"#{reflection.foreign_key}" => value.try(:id)}
|
66
|
+
when :has_one
|
67
|
+
{:"_#{name.to_s.singularize}_id" => value.try(:id)}
|
68
|
+
when :has_many, :has_and_belongs_to_many
|
69
|
+
{:"_#{name.to_s.singularize}_ids" => value.collect(&:id)}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module CachedRecord
|
2
|
+
module ORM
|
3
|
+
module DataMapper
|
4
|
+
|
5
|
+
def self.setup?
|
6
|
+
!!defined?(::DataMapper)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.setup
|
10
|
+
return false unless setup?
|
11
|
+
::DataMapper::Resource.class_eval do
|
12
|
+
class << self
|
13
|
+
alias :included_without_cached_record :included
|
14
|
+
end
|
15
|
+
def self.included(base)
|
16
|
+
included_without_cached_record base
|
17
|
+
base.extend ORM::ClassMethods
|
18
|
+
base.extend ClassMethods
|
19
|
+
end
|
20
|
+
end
|
21
|
+
::DataMapper::Resource.send :include, ORM::InstanceMethods
|
22
|
+
::DataMapper::Resource.send :include, InstanceMethods
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def uncached(id)
|
28
|
+
get(id)
|
29
|
+
end
|
30
|
+
def new_cached_instance(attributes, foreign_keys, variables)
|
31
|
+
super.tap do |instance|
|
32
|
+
instance.persistence_state = ::DataMapper::Resource::PersistenceState::Clean.new instance
|
33
|
+
end
|
34
|
+
end
|
35
|
+
private
|
36
|
+
def _new_cached_instance_(id, attributes)
|
37
|
+
new attributes.merge(:id => id)
|
38
|
+
end
|
39
|
+
def set_cached_association(instance, key, value)
|
40
|
+
return unless value
|
41
|
+
if relationship = _cache_relationship_(instance, key, value)
|
42
|
+
value = _cache_value_(instance, relationship, value)
|
43
|
+
instance.instance_variable_set relationship.instance_variable_name, value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
def _cache_relationship_(instance, key, value)
|
47
|
+
if key.to_s.match /^_(.*)_ids?/
|
48
|
+
relationships[$1.pluralize.to_sym]
|
49
|
+
else
|
50
|
+
instance.send :"#{key}=", value
|
51
|
+
relationships.detect{|r| r.child_key.first.name.to_s == key.to_s}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
def _cache_value_(instance, relationship, value)
|
55
|
+
if value.is_a? Array
|
56
|
+
value.collect{|x| relationship.child_model.cached x}
|
57
|
+
elsif relationship.is_a?(::DataMapper::Associations::ManyToOne::Relationship)
|
58
|
+
relationship.parent_model.cached value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module InstanceMethods
|
64
|
+
def cache_attributes
|
65
|
+
(cache_json_options[:only] ? as_json.slice(*cache_json_options[:only]) : as_json).symbolize_keys!.merge cache_foreign_keys
|
66
|
+
end
|
67
|
+
def cache_foreign_keys
|
68
|
+
(cache_json_options[:include] || {}).inject({}) do |json, name|
|
69
|
+
relationship = relationships[name]
|
70
|
+
json.merge cache_foreign_key(name, relationship, send(name))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
def cache_foreign_key(name, relationship, value)
|
74
|
+
case relationship
|
75
|
+
when ::DataMapper::Associations::ManyToOne::Relationship
|
76
|
+
{:"#{relationship.child_key.first.name}" => value.try(:id)}
|
77
|
+
when ::DataMapper::Associations::OneToOne::Relationship
|
78
|
+
{:"_#{name.to_s.singularize}_id" => value.try(:id)}
|
79
|
+
when ::DataMapper::Associations::OneToMany::Relationship, ::DataMapper::Associations::ManyToMany::Relationship
|
80
|
+
{:"_#{name.to_s.singularize}_ids" => value.collect(&:id)}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "cached_record/orm/active_record"
|
2
|
+
require "cached_record/orm/data_mapper"
|
3
|
+
|
4
|
+
module CachedRecord
|
5
|
+
module ORM
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.send :include, InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def as_cache(*args)
|
15
|
+
@as_cache = parse_as_cache_options args if args.any?
|
16
|
+
@as_cache ||= {:as_json => {}}
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_memoized_cache(*args)
|
20
|
+
retain = args.last.delete(:retain) if args.last.is_a?(Hash)
|
21
|
+
as_cache(*args).tap do |options|
|
22
|
+
options[:memoize] = true
|
23
|
+
options[:retain] = retain if retain
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_key(id)
|
28
|
+
"#{name.underscore.gsub("/", ".")}.#{id}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def cache_root
|
32
|
+
"#{name.underscore.gsub(/^.*\//, "")}".to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
def cached(id)
|
36
|
+
Cache.get(self, id) do
|
37
|
+
uncached id
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def uncached(id)
|
42
|
+
raise NotImplementedError, "Cannot fetch uncached `#{self.class}` instances"
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_cache_json(json)
|
46
|
+
json.symbolize_keys!
|
47
|
+
properties, variables = cache_json_to_properties_and_variables(json)
|
48
|
+
foreign_keys, attributes = properties.partition{|k, v| k.to_s.match /_ids?$/}.collect{|x| Hash[x]}
|
49
|
+
new_cached_instance attributes, foreign_keys, variables
|
50
|
+
end
|
51
|
+
|
52
|
+
def new_cached_instance(attributes, foreign_keys, variables)
|
53
|
+
id = attributes.delete(:id) || attributes.delete("id")
|
54
|
+
_new_cached_instance_(id, attributes).tap do |instance|
|
55
|
+
instance.id = id if instance.respond_to?(:id=)
|
56
|
+
foreign_keys.each do |key, value|
|
57
|
+
set_cached_association instance, key, value
|
58
|
+
end
|
59
|
+
variables.each do |key, value|
|
60
|
+
instance.instance_variable_set key, value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def _new_cached_instance_(id, attributes)
|
68
|
+
new attributes
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_cached_association(instance, key, value)
|
72
|
+
raise NotImplementedError, "Cannot set cached association for `#{self}` instances"
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_as_cache_options(args)
|
76
|
+
if (symbol = args.first).is_a? Symbol
|
77
|
+
store = symbol
|
78
|
+
end
|
79
|
+
if (hash = args.last).is_a? Hash
|
80
|
+
expire = hash.delete :expire
|
81
|
+
as_json = parse_as_cache_json_options hash
|
82
|
+
end
|
83
|
+
{
|
84
|
+
:store => store,
|
85
|
+
:expire => expire,
|
86
|
+
:as_json => as_json || {}
|
87
|
+
}.reject{|key, value| value.nil?}
|
88
|
+
end
|
89
|
+
|
90
|
+
def parse_as_cache_json_options(options)
|
91
|
+
options.symbolize_keys!
|
92
|
+
validate_as_cache_json_options options
|
93
|
+
{}.tap do |opts|
|
94
|
+
opts[:only] = symbolize_array(options[:only]) if options[:only]
|
95
|
+
opts[:include] = symbolize_array(options[:include]) if options[:include]
|
96
|
+
opts[:memoize] = parse_memoize_options(options[:memoize]) if options[:memoize]
|
97
|
+
opts[:include_root] = true if options[:include_root]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_as_cache_json_options(options)
|
102
|
+
options.assert_valid_keys :only, :include, :memoize, :include_root
|
103
|
+
options.slice(:only, :include).each do |key, value|
|
104
|
+
raise ArgumentError unless value.is_a?(Array)
|
105
|
+
end
|
106
|
+
if options[:memoize] && !options[:memoize].is_a?(Enumerable)
|
107
|
+
raise ArgumentError
|
108
|
+
end
|
109
|
+
if options.include?(:include_root) && ![true, false].include?(options[:include_root])
|
110
|
+
raise ArgumentError
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def symbolize_array(array)
|
115
|
+
array.collect &:to_sym
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_memoize_options(options)
|
119
|
+
[options].flatten.inject({}) do |memo, x|
|
120
|
+
hash = x.is_a?(Hash) ? x : {x => :"@#{x}"}
|
121
|
+
memo.merge hash.inject({}){|h, (k, v)| h[k.to_sym] = v.to_sym; h}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def cache_json_to_properties_and_variables(json)
|
126
|
+
if as_cache[:as_json][:include_root]
|
127
|
+
properties = json.delete cache_root
|
128
|
+
variables = json.inject({}){|h, (k, v)| h[:"@#{k}"] = v; h}
|
129
|
+
[properties, variables]
|
130
|
+
else
|
131
|
+
json.partition{|k, v| !k.to_s.match(/^@/)}.collect{|x| Hash[x]}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
module InstanceMethods
|
138
|
+
|
139
|
+
def cache_attributes
|
140
|
+
raise NotImplementedError, "Cannot return cache attributes for `#{self.class}` instances"
|
141
|
+
end
|
142
|
+
|
143
|
+
def cache_foreign_keys
|
144
|
+
raise NotImplementedError, "Cannot return cache foreign keys for `#{self.class}` instances"
|
145
|
+
end
|
146
|
+
|
147
|
+
def as_cache_json
|
148
|
+
attributes = {:id => id}.merge cache_attributes
|
149
|
+
variables = (cache_json_options[:memoize] || {}).inject({}) do |hash, (method, variable)|
|
150
|
+
hash[variable] = send method
|
151
|
+
hash
|
152
|
+
end
|
153
|
+
merge_cache_json attributes, variables
|
154
|
+
end
|
155
|
+
|
156
|
+
def merge_cache_json(attributes, variables)
|
157
|
+
if cache_json_options[:include_root]
|
158
|
+
variables = variables.inject({}){|h, (k, v)| h[k.to_s.gsub(/^@/, "").to_sym] = v; h}
|
159
|
+
{self.class.cache_root => attributes}.merge variables
|
160
|
+
else
|
161
|
+
attributes.merge variables
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_cache_json
|
166
|
+
as_cache_json.to_json
|
167
|
+
end
|
168
|
+
|
169
|
+
def cache
|
170
|
+
self.class.cached id
|
171
|
+
true
|
172
|
+
end
|
173
|
+
|
174
|
+
def expire
|
175
|
+
Cache.expire self
|
176
|
+
true
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def cache_json_options
|
182
|
+
self.class.as_cache[:as_json] || {}
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "gem_ext/redis"
|
2
|
+
require "cached_record/version"
|
3
|
+
require "cached_record/cache"
|
4
|
+
require "cached_record/orm"
|
5
|
+
|
6
|
+
module CachedRecord
|
7
|
+
|
8
|
+
def self.setup(*args)
|
9
|
+
args.each do |arg|
|
10
|
+
if arg.is_a?(Hash)
|
11
|
+
arg.each do |store, options|
|
12
|
+
Cache.setup store, options
|
13
|
+
end
|
14
|
+
else
|
15
|
+
Cache.setup arg
|
16
|
+
end
|
17
|
+
end
|
18
|
+
ORM::ActiveRecord.setup
|
19
|
+
ORM::DataMapper.setup
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
begin
|
2
|
+
require "redis"
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
class Redis
|
7
|
+
|
8
|
+
alias :set_without_cached_record :set
|
9
|
+
def set(key, value, ttl_or_options = nil)
|
10
|
+
if ttl_or_options.is_a? Integer
|
11
|
+
ttl = ttl_or_options
|
12
|
+
options = {}
|
13
|
+
else
|
14
|
+
options = ttl_or_options || {}
|
15
|
+
end
|
16
|
+
set_without_cached_record(key, value, options).tap do
|
17
|
+
expire key, ttl if ttl
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(*args)
|
22
|
+
del *args
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/log/.gitkeep
ADDED
File without changes
|
data/script/console
ADDED
data/script/setup.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "logger"
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
dbconfig = YAML.load_file(File.expand_path("../../config/database.yml", __FILE__))["development"]
|
6
|
+
logger = Logger.new("log/development.log")
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection dbconfig
|
9
|
+
ActiveRecord::Base.time_zone_aware_attributes = true
|
10
|
+
ActiveRecord::Base.default_timezone = :local
|
11
|
+
ActiveRecord::Base.logger = logger
|
12
|
+
|
13
|
+
CachedRecord.setup :redis
|
14
|
+
Redis.new.flushdb
|
15
|
+
|
16
|
+
class Article < ActiveRecord::Base
|
17
|
+
belongs_to :author, :class_name => "User"
|
18
|
+
has_many :comments
|
19
|
+
has_and_belongs_to_many :tags
|
20
|
+
as_memoized_cache :only => [:title], :include => [:author, :comments, :tags], :expire => 20.seconds
|
21
|
+
end
|
22
|
+
|
23
|
+
class User < ActiveRecord::Base
|
24
|
+
has_one :foo, :class_name => "Article", :foreign_key => "foo_id"
|
25
|
+
as_memoized_cache :only => [:name], :include => [:foo]
|
26
|
+
end
|
27
|
+
|
28
|
+
class Comment < ActiveRecord::Base
|
29
|
+
belongs_to :article
|
30
|
+
belongs_to :poster, :class_name => "User"
|
31
|
+
as_memoized_cache :only => [:content], :include => [:poster]
|
32
|
+
end
|
33
|
+
|
34
|
+
class Tag < ActiveRecord::Base
|
35
|
+
has_and_belongs_to_many :articles
|
36
|
+
as_memoized_cache :only => [:name]
|
37
|
+
end
|