memcacheable 0.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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +151 -0
- data/Rakefile +5 -0
- data/lib/memcacheable.rb +71 -0
- data/lib/memcacheable/fetch_association.rb +22 -0
- data/lib/memcacheable/fetch_belongs_to.rb +11 -0
- data/lib/memcacheable/fetch_by.rb +15 -0
- data/lib/memcacheable/fetch_by_criteria.rb +16 -0
- data/lib/memcacheable/fetch_has_many.rb +16 -0
- data/lib/memcacheable/fetch_has_one.rb +16 -0
- data/lib/memcacheable/fetch_one.rb +22 -0
- data/lib/memcacheable/fetch_where.rb +15 -0
- data/lib/memcacheable/fetcher.rb +19 -0
- data/lib/memcacheable/flusher.rb +30 -0
- data/lib/memcacheable/version.rb +3 -0
- data/memcacheable.gemspec +28 -0
- data/spec/lib/memcacheable_spec.rb +272 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/fake_model.rb +60 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0260c3ba7467a592bc787a2fb66355703ef1d31f
|
4
|
+
data.tar.gz: 093f304c2591ff664e671f03f6972f2cf0c9b4f2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3dbd55c9b08ad60a26a148c2659c2052cd00806b6d3d9d4e5214922b728418616ae73b7e1e803946b9a457b1c3b1ee230b1f79048d53f5e02260767be6b16ea7
|
7
|
+
data.tar.gz: 6cfeed9c6dd105f8aafe8c2b5d30e1b461f27ca35ba84d753350bf65952bb535774be1226b9d4b36c6104730d2dd755c9bb56f584bc34bb690e45cab0a596a2c
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Scott McCormack
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# Memcacheable [](https://travis-ci.org/flintinatux/memcacheable) [](https://gemnasium.com/flintinatux/memcacheable) [](https://codeclimate.com/github/flintinatux/memcacheable)
|
2
|
+
|
3
|
+
A Rails concern to make caching ActiveRecord objects as dead-simple as possible. Uses the built-in Rails.cache mechanism, and implements the new finder methods available in Rails 4.0 to ensure maximum future compatibility.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'memcacheable'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install memcacheable
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Let's do some caching!
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Person < ActiveRecord::Base
|
25
|
+
include Memcacheable
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Boom! Now you can `fetch` a person by their id, like below. When the person gets updated or touched, it will flush the cache, and the person will be reloaded on the next `fetch`.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
person = Person.fetch id # caches the person
|
33
|
+
person.touch # flushes the cache
|
34
|
+
person = Person.fetch id # the cache misses, and the person is reloaded
|
35
|
+
```
|
36
|
+
|
37
|
+
### Cache by criteria
|
38
|
+
|
39
|
+
That's easy-sauce. Time to step up our caching game! _"I want to cache queries by criteria, not just id's!"_ No probs:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class Person < ActiveRecord::Base
|
43
|
+
include Memcacheable
|
44
|
+
cache_index :name
|
45
|
+
cache_index :height, :weight
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
Powerhouse time! `cache_index` adds these index combinations to the list of cacheable things, so we can fetch single records with `fetch_by`, like this:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
person = Person.fetch_by name: 'Scott' # caches an awesome dude
|
53
|
+
person.update_attributes name: 'Scottie' # flushes the cache
|
54
|
+
person = Person.fetch_by name: 'Scott' # => nil (he's got a new identity!)
|
55
|
+
|
56
|
+
# You can also do multiple criteria, and order doesn't matter.
|
57
|
+
person = Person.fetch_by weight: 175, height: 72
|
58
|
+
person.update_attributes height: 71 # he shrunk? oh well, cache flushed
|
59
|
+
person = Person.fetch_by weight: 175, height: 71 # fetched and cached with new height
|
60
|
+
```
|
61
|
+
|
62
|
+
Like noise in your life? Try `fetch_by!` (hard to say: "fetch-by-_bang!_").
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
person = Person.fetch_by! name: 'Mork' # => ActiveRecord::RecordNotFound
|
66
|
+
```
|
67
|
+
|
68
|
+
While `fetch_by` just pulls back just one record, you can fetch a collection with `fetch_where`:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
people = Person.fetch_where weight: 42, height: 120 # => an array of tall, skinny people
|
72
|
+
people.first.update_attributes weight: 43 # one guy gained a little weight --> cache flushed
|
73
|
+
people = Person.fetch_where weight: 42, height: 120 # => an array of everyone but that first guy
|
74
|
+
```
|
75
|
+
|
76
|
+
Trying to `fetch_by` or `fetch_where` by criteria that you didn't specify with `cache_index` will raise an error, because Memcacheable won't know how to bust the cache when things get changed. For example:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
Person.fetch_by name: 'Scott' # good
|
80
|
+
Person.fetch_by favorite_color: 'red' # shame on you! this wasn't cache_index'd!
|
81
|
+
```
|
82
|
+
|
83
|
+
**Caveat:** hash-style criteria is currently **required** for `fetch_by` and `fetch_where`. Something like `person.fetch_where 'height < ?', 71` will raise an error.
|
84
|
+
|
85
|
+
Btw, don't do something stupid like trying to call scope methods on the result of a `fetch_where`. It returns an `Array`, not an `ActiveRecord::Relation`. That means this will blow up on you:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
Person.fetch_where(height: 60).limit(5)
|
89
|
+
```
|
90
|
+
|
91
|
+
I may fix this later, though, because I like scopes.
|
92
|
+
|
93
|
+
### Cache associations
|
94
|
+
|
95
|
+
If you love Rails, then you know you love ActiveRecord associations. Memcacheable loves them too. Check this out:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class Person < ActiveRecord::Base
|
99
|
+
has_one :dog
|
100
|
+
has_many :kittens
|
101
|
+
|
102
|
+
include Memcacheable
|
103
|
+
cache_has_one :dog
|
104
|
+
cache_has_many :kittens
|
105
|
+
end
|
106
|
+
|
107
|
+
class Dog < ActiveRecord::Base
|
108
|
+
belongs_to :person, touch: true
|
109
|
+
|
110
|
+
include Memcacheable
|
111
|
+
cache_belongs_to :person
|
112
|
+
end
|
113
|
+
|
114
|
+
class Kitten < ActiveRecord::Base
|
115
|
+
belongs_to :person, touch: true
|
116
|
+
|
117
|
+
include Memcacheable
|
118
|
+
cache_index :person_id
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
**Notice** the `touch: true` above. That's important to bust the parent cache when a child record is updated!
|
123
|
+
|
124
|
+
So what do we get with all of this caching magic? Why a bunch of dynamic association fetch methods of course! Observe:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
dog = person.fetch_dog # his name's Bruiser, btw
|
128
|
+
dog.update_attributes name: 'Fido' # flushes the cached association on the person
|
129
|
+
dog = person.fetch_dog # finds and caches Fido with his new name
|
130
|
+
dog.fetch_person # gets the cached owner
|
131
|
+
```
|
132
|
+
|
133
|
+
For a slight optimization, specify a `cache_index` on the foreign key of the association, like in the `Kitten` example above. Memcacheable will then do a `fetch_by` or `fetch_where` as appropriate. The cost: two copies in the cache. The gain: when the parent changes but the children don't, the children can be reloaded from the cache. Like this:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
person.fetch_kittens # caches the kittens both by criteria and as an association
|
137
|
+
person.touch # association cache is flushed, but not the fetch_where
|
138
|
+
person.fetch_kittens # reloads the kittens from the cache, and caches as an association
|
139
|
+
```
|
140
|
+
|
141
|
+
## Inspiration
|
142
|
+
|
143
|
+
None of the caching options out there really satisfied my needs, so I wrote this gem. But I was not without inspiration. I learned the basics of Rails caching from the [RailsCasts](http://railscasts.com/) episode on [Model Caching](http://railscasts.com/episodes/115-model-caching-revised), and I borrowed a lot of syntax from the very popular [IdentityCache gem](https://github.com/Shopify/identity_cache) from our friends at [Shopify](http://www.shopify.com/).
|
144
|
+
|
145
|
+
## Contributing
|
146
|
+
|
147
|
+
1. Fork it
|
148
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
149
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
150
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
151
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/memcacheable.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'memcacheable/version'
|
2
|
+
|
3
|
+
module Memcacheable
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
autoload :FetchAssociation
|
8
|
+
autoload :FetchBelongsTo
|
9
|
+
autoload :FetchBy
|
10
|
+
autoload :FetchByCriteria
|
11
|
+
autoload :FetchHasMany
|
12
|
+
autoload :FetchHasOne
|
13
|
+
autoload :FetchOne
|
14
|
+
autoload :FetchWhere
|
15
|
+
autoload :Fetcher
|
16
|
+
autoload :Flusher
|
17
|
+
|
18
|
+
included do
|
19
|
+
cattr_accessor :cached_indexes do []; end
|
20
|
+
after_commit :flush_cache
|
21
|
+
end
|
22
|
+
|
23
|
+
def flush_cache
|
24
|
+
Flusher.new(self).flush
|
25
|
+
end
|
26
|
+
|
27
|
+
def touch
|
28
|
+
super
|
29
|
+
flush_cache
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
def cache_belongs_to(association)
|
34
|
+
define_method "fetch_#{association}" do
|
35
|
+
FetchBelongsTo.new(self, association).fetch
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache_has_one(association)
|
40
|
+
define_method "fetch_#{association}" do
|
41
|
+
FetchHasOne.new(self, association).fetch
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def cache_has_many(association)
|
46
|
+
define_method "fetch_#{association}" do
|
47
|
+
FetchHasMany.new(self, association).fetch
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def cache_index(*fields)
|
52
|
+
self.cached_indexes << fields.map(&:to_sym).sort
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch(id)
|
56
|
+
FetchOne.new(self, id).fetch
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_by(*args)
|
60
|
+
FetchBy.new(self, *args).fetch
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_by!(*args)
|
64
|
+
fetch_by(*args) || raise(ActiveRecord::RecordNotFound)
|
65
|
+
end
|
66
|
+
|
67
|
+
def fetch_where(*args)
|
68
|
+
FetchWhere.new(self, *args).fetch
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchAssociation < Fetcher
|
3
|
+
attr_accessor :object, :association
|
4
|
+
|
5
|
+
def initialize(object, association)
|
6
|
+
self.object = object
|
7
|
+
self.association = association
|
8
|
+
end
|
9
|
+
|
10
|
+
def cache_key
|
11
|
+
[object, association]
|
12
|
+
end
|
13
|
+
|
14
|
+
def class_name
|
15
|
+
object.class.name.downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
"#{association} for #{class_name} #{object.id}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchBelongsTo < FetchAssociation
|
3
|
+
|
4
|
+
def find_on_cache_miss
|
5
|
+
klass = association.to_s.camelize.constantize
|
6
|
+
id = object.send "#{association}_id"
|
7
|
+
klass.respond_to?(:fetch) ? klass.fetch(id) : klass.find(id) rescue nil
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchBy < FetchByCriteria
|
3
|
+
def cache_key
|
4
|
+
{what: klass.name.downcase}.merge criteria
|
5
|
+
end
|
6
|
+
|
7
|
+
def description
|
8
|
+
"#{klass.name.downcase} with #{criteria.inspect}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_on_cache_miss
|
12
|
+
klass.find_by criteria
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchByCriteria < Fetcher
|
3
|
+
attr_accessor :klass, :criteria
|
4
|
+
|
5
|
+
def criteria_cacheable?
|
6
|
+
klass.cached_indexes.include? criteria.keys.map(&:to_sym).sort
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(klass, *args)
|
10
|
+
self.klass = klass
|
11
|
+
self.criteria = args.extract_options!
|
12
|
+
raise "Only hash-style args accepted! Illegal args: #{args.inspect}" if args.any?
|
13
|
+
raise "No cache_index found in #{klass.name} matching fields #{criteria.keys.inspect}!" unless criteria_cacheable?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchHasMany < FetchAssociation
|
3
|
+
def fetchable?
|
4
|
+
klass.respond_to?(:fetch_where) && klass.cached_indexes.include?(["#{class_name}_id".to_sym])
|
5
|
+
end
|
6
|
+
|
7
|
+
def find_on_cache_miss
|
8
|
+
criteria = { "#{class_name}_id" => object.id }
|
9
|
+
fetchable? ? klass.fetch_where(criteria) : klass.where(criteria).to_a
|
10
|
+
end
|
11
|
+
|
12
|
+
def klass
|
13
|
+
@klass ||= association.to_s.classify.constantize
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchHasOne < FetchAssociation
|
3
|
+
def fetchable?
|
4
|
+
klass.respond_to?(:fetch_by) && klass.cached_indexes.include?(["#{class_name}_id".to_sym])
|
5
|
+
end
|
6
|
+
|
7
|
+
def find_on_cache_miss
|
8
|
+
criteria = { "#{class_name}_id" => object.id }
|
9
|
+
fetchable? ? klass.fetch_by(criteria) : klass.find_by(criteria)
|
10
|
+
end
|
11
|
+
|
12
|
+
def klass
|
13
|
+
@klass ||= association.to_s.camelize.constantize
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchOne < Fetcher
|
3
|
+
attr_accessor :klass, :id
|
4
|
+
|
5
|
+
def initialize(klass, id)
|
6
|
+
self.klass = klass
|
7
|
+
self.id = id
|
8
|
+
end
|
9
|
+
|
10
|
+
def cache_key
|
11
|
+
[klass.name.downcase, id]
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"#{klass.name.downcase} #{id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_on_cache_miss
|
19
|
+
klass.find id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class FetchWhere < FetchByCriteria
|
3
|
+
def cache_key
|
4
|
+
{what: klass.name.tableize}.merge criteria
|
5
|
+
end
|
6
|
+
|
7
|
+
def description
|
8
|
+
"#{klass.name.tableize} with #{criteria.inspect}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_on_cache_miss
|
12
|
+
klass.where(criteria).to_a
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class Fetcher
|
3
|
+
def debug(action)
|
4
|
+
Rails.logger.debug "[memcacheable] #{action} #{description}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def fetch
|
8
|
+
debug :read
|
9
|
+
Rails.cache.fetch cache_key do
|
10
|
+
debug :write
|
11
|
+
find_on_cache_miss
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def flush
|
16
|
+
Rails.cache.delete cache_key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Memcacheable
|
2
|
+
class Flusher
|
3
|
+
attr_accessor :object
|
4
|
+
|
5
|
+
OLD_VAL = 0
|
6
|
+
NEW_VAL = 1
|
7
|
+
|
8
|
+
def initialize(object)
|
9
|
+
self.object = object
|
10
|
+
end
|
11
|
+
|
12
|
+
def changed_criteria_for(which, fields)
|
13
|
+
fields.inject({}) do |hash, field|
|
14
|
+
value = object.previous_changes[field][which] rescue object.send(field)
|
15
|
+
hash.merge! field => value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def flush
|
20
|
+
FetchOne.new(object.class, object.id).flush
|
21
|
+
object.cached_indexes.each do |fields|
|
22
|
+
[OLD_VAL,NEW_VAL].each do |which|
|
23
|
+
criteria = changed_criteria_for which, fields
|
24
|
+
FetchBy.new(object.class, criteria).flush
|
25
|
+
FetchWhere.new(object.class, criteria).flush
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'memcacheable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'memcacheable'
|
8
|
+
spec.version = Memcacheable::VERSION
|
9
|
+
spec.authors = ['Scott McCormack']
|
10
|
+
spec.email = ['flintinatux@gmail.com']
|
11
|
+
spec.description = %q{A Rails concern to make caching ActiveRecord objects as dead-simple as possible. Uses the built-in Rails.cache mechanism, and implements the new finder methods available in Rails 4.0 to ensure maximum future compatibility.}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = 'https://github.com/flintinatux/memcacheable'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'activerecord', '>= 4.0.0'
|
22
|
+
spec.add_dependency 'activesupport', '>= 4.0.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'activemodel', '>= 4.0.0'
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 2.13'
|
28
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Person < FakeModel
|
4
|
+
define_attribute :color, :number, :dog_id
|
5
|
+
|
6
|
+
include Memcacheable
|
7
|
+
cache_index :number
|
8
|
+
cache_index :color, :number
|
9
|
+
cache_has_one :dog
|
10
|
+
cache_has_many :kittens
|
11
|
+
end
|
12
|
+
|
13
|
+
class Dog < FakeModel
|
14
|
+
define_attribute :person_id
|
15
|
+
include Memcacheable
|
16
|
+
end
|
17
|
+
|
18
|
+
class Kitten < FakeModel
|
19
|
+
define_attribute :person_id
|
20
|
+
include Memcacheable
|
21
|
+
cache_index :person_id
|
22
|
+
cache_belongs_to :person
|
23
|
+
end
|
24
|
+
|
25
|
+
describe Memcacheable do
|
26
|
+
let(:id) { 123 }
|
27
|
+
let(:person) { Person.new id: id, color: 'red', number: 42 }
|
28
|
+
|
29
|
+
before :all do
|
30
|
+
unless defined?(Rails)
|
31
|
+
class Rails; cattr_accessor :cache, :logger; end
|
32
|
+
Rails.cache = ActiveSupport::Cache::MemoryStore.new
|
33
|
+
Rails.logger = ActiveSupport::Logger.new(STDOUT)
|
34
|
+
Rails.logger.level = 3
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
after { Rails.cache.clear }
|
39
|
+
|
40
|
+
describe '::fetch' do
|
41
|
+
before do
|
42
|
+
Person.stub(:find).with(id).and_return person
|
43
|
+
end
|
44
|
+
|
45
|
+
it "finds the correct record" do
|
46
|
+
Person.fetch(id).should eq person
|
47
|
+
end
|
48
|
+
|
49
|
+
it "only queries once and then caches" do
|
50
|
+
Person.should_receive(:find).once
|
51
|
+
Person.fetch id
|
52
|
+
Person.fetch id
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#touch' do
|
57
|
+
before do
|
58
|
+
Person.stub(:find).with(id).and_return person
|
59
|
+
end
|
60
|
+
|
61
|
+
it "flushes the cache" do
|
62
|
+
Person.fetch(id).touch
|
63
|
+
Person.should_receive(:find).once
|
64
|
+
Person.fetch id
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '::fetch_by' do
|
69
|
+
let(:good_criteria) {{ number: 42, color: 'red' }}
|
70
|
+
|
71
|
+
before do
|
72
|
+
Person.stub(:find_by) do |criteria|
|
73
|
+
criteria.all?{ |k,v| person.send(k) == v } ? person : nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it "finds the correct record" do
|
78
|
+
Person.fetch_by(good_criteria).should eq person
|
79
|
+
end
|
80
|
+
|
81
|
+
it "only queries once and then caches" do
|
82
|
+
Person.should_receive(:find_by).once
|
83
|
+
Person.fetch_by good_criteria
|
84
|
+
Person.fetch_by good_criteria
|
85
|
+
end
|
86
|
+
|
87
|
+
it "raises on non-hash style args" do
|
88
|
+
expect { Person.fetch_by 'color = ?', 'red' }.to raise_error
|
89
|
+
end
|
90
|
+
|
91
|
+
it "raises on non-cached indexes" do
|
92
|
+
expect { Person.fetch_by color: 'red' }.to raise_error
|
93
|
+
end
|
94
|
+
|
95
|
+
it "flushes when model updated" do
|
96
|
+
person = Person.fetch_by good_criteria
|
97
|
+
person.update_attributes number: 7
|
98
|
+
Person.should_receive(:find_by).once
|
99
|
+
Person.fetch_by good_criteria
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '::fetch_by!' do
|
104
|
+
before do
|
105
|
+
Person.stub(:find_by).and_return nil
|
106
|
+
end
|
107
|
+
|
108
|
+
it "raises error on nil result" do
|
109
|
+
expect { Person.fetch_by!(number: 42) }.to raise_error(ActiveRecord::RecordNotFound)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'flushing cached_indexes' do
|
114
|
+
let(:old_num) {{ number: 42 }}
|
115
|
+
let(:new_num) {{ number: 21 }}
|
116
|
+
let(:old_person) {{ person_id: id }}
|
117
|
+
let(:new_person) {{ person_id: 345 }}
|
118
|
+
let(:kittens) { 3.times.map { Kitten.new person_id: id} }
|
119
|
+
|
120
|
+
before do
|
121
|
+
Person.stub(:find_by) do |criteria|
|
122
|
+
criteria.all?{ |k,v| person.send(k) == v } ? person : nil
|
123
|
+
end
|
124
|
+
Kitten.stub(:where) do |criteria|
|
125
|
+
kittens.select do |kitten|
|
126
|
+
criteria.all? { |k,v| kitten.send(k) == v }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it "flushes old and new values for cached_indexes" do
|
132
|
+
Person.fetch_by(old_num).should eq person
|
133
|
+
Person.fetch_by(new_num).should eq nil
|
134
|
+
person.update_attributes new_num
|
135
|
+
Person.fetch_by(old_num).should eq nil
|
136
|
+
Person.fetch_by(new_num).should eq person
|
137
|
+
|
138
|
+
Kitten.fetch_where(old_person).should eq kittens
|
139
|
+
Kitten.fetch_where(new_person).should eq []
|
140
|
+
kittens.each { |k| k.update_attributes new_person }
|
141
|
+
Kitten.fetch_where(old_person).should eq []
|
142
|
+
Kitten.fetch_where(new_person).should eq kittens
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe '::cache_belongs_to' do
|
147
|
+
let(:kitten) { Kitten.new person_id: id }
|
148
|
+
before do
|
149
|
+
Person.stub(:find).and_return nil
|
150
|
+
Person.stub(:find).with(id).and_return person
|
151
|
+
end
|
152
|
+
|
153
|
+
it "defines a new fetch method" do
|
154
|
+
kitten.should.respond_to? :fetch_person
|
155
|
+
end
|
156
|
+
|
157
|
+
it "fetches the correct object" do
|
158
|
+
kitten.fetch_person.should eq person
|
159
|
+
end
|
160
|
+
|
161
|
+
it "only queries once and then caches" do
|
162
|
+
Person.should_receive(:fetch).once
|
163
|
+
kitten.fetch_person
|
164
|
+
kitten.fetch_person
|
165
|
+
end
|
166
|
+
|
167
|
+
it "flushes when touched by association" do
|
168
|
+
kitten.fetch_person
|
169
|
+
kitten.touch
|
170
|
+
Person.should_receive(:fetch).once
|
171
|
+
kitten.fetch_person
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe '::cache_has_one' do
|
176
|
+
let(:dog) { Dog.new person_id: id }
|
177
|
+
before do
|
178
|
+
Dog.stub(:find_by) do |criteria|
|
179
|
+
criteria.all?{ |k,v| dog.send(k) == v } ? dog : nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
it "defines a new fetch method" do
|
184
|
+
person.should.respond_to? :fetch_dog
|
185
|
+
end
|
186
|
+
|
187
|
+
it "fetches the correct object" do
|
188
|
+
person.fetch_dog.should eq dog
|
189
|
+
end
|
190
|
+
|
191
|
+
it "only queries once and then caches" do
|
192
|
+
Dog.should_receive(:find_by).once
|
193
|
+
person.fetch_dog
|
194
|
+
person.fetch_dog
|
195
|
+
end
|
196
|
+
|
197
|
+
it "flushes when touched by association" do
|
198
|
+
person.fetch_dog
|
199
|
+
person.touch
|
200
|
+
Dog.should_receive(:find_by).once
|
201
|
+
person.fetch_dog
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe '::fetch_where' do
|
206
|
+
let(:kittens) { 3.times.map { Kitten.new person_id: id} }
|
207
|
+
before do
|
208
|
+
Kitten.stub(:where) do |criteria|
|
209
|
+
kittens.select do |kitten|
|
210
|
+
criteria.all? { |k,v| kitten.send(k) == v }
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
it "fetches the correct objects" do
|
216
|
+
Kitten.fetch_where(person_id: id).should eq kittens
|
217
|
+
end
|
218
|
+
|
219
|
+
it "only queries once and then caches" do
|
220
|
+
Kitten.should_receive(:where).once
|
221
|
+
Kitten.fetch_where person_id: id
|
222
|
+
Kitten.fetch_where person_id: id
|
223
|
+
end
|
224
|
+
|
225
|
+
it "raises on non-hash style args" do
|
226
|
+
expect { Kitten.fetch_where 'color = ?', 'red' }.to raise_error
|
227
|
+
end
|
228
|
+
|
229
|
+
it "raises on non-cached indexes" do
|
230
|
+
expect { Kitten.fetch_where id: 123 }.to raise_error
|
231
|
+
end
|
232
|
+
|
233
|
+
it "flushes when model updated" do
|
234
|
+
kittens = Kitten.fetch_where person_id: id
|
235
|
+
kittens.first.update_attributes person_id: 345
|
236
|
+
Kitten.should_receive(:where).once
|
237
|
+
Kitten.fetch_where person_id: id
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe '::cache_has_many' do
|
242
|
+
let(:kittens) { 3.times.map { Kitten.new person_id: id} }
|
243
|
+
before do
|
244
|
+
Kitten.stub(:where) do |criteria|
|
245
|
+
kittens.select do |kitten|
|
246
|
+
criteria.all? { |k,v| kitten.send(k) == v }
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
it "defines a new fetch method" do
|
252
|
+
person.should.respond_to? :fetch_kittens
|
253
|
+
end
|
254
|
+
|
255
|
+
it "fetches the correct collection" do
|
256
|
+
person.fetch_kittens.should eq kittens
|
257
|
+
end
|
258
|
+
|
259
|
+
it "only queries once and then caches" do
|
260
|
+
Kitten.should_receive(:fetch_where).once
|
261
|
+
person.fetch_kittens
|
262
|
+
person.fetch_kittens
|
263
|
+
end
|
264
|
+
|
265
|
+
it "flushes when touched by association" do
|
266
|
+
person.fetch_kittens
|
267
|
+
person.touch
|
268
|
+
Kitten.should_receive(:fetch_where).once
|
269
|
+
person.fetch_kittens
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support'
|
4
|
+
require 'memcacheable'
|
5
|
+
|
6
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
7
|
+
# in spec/support/ and its subdirectories.
|
8
|
+
Dir[File.join File.dirname(__FILE__), 'support/**/*.rb'].each {|f| require f}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class FakeModel
|
2
|
+
include ActiveModel::Dirty
|
3
|
+
extend ActiveModel::Callbacks
|
4
|
+
|
5
|
+
define_model_callbacks :commit
|
6
|
+
|
7
|
+
def self.define_attribute(*attrs)
|
8
|
+
attrs.each do |attr_name|
|
9
|
+
define_attribute_method attr_name
|
10
|
+
attr_reader attr_name
|
11
|
+
define_method "#{attr_name}=" do |value|
|
12
|
+
send "#{attr_name}_will_change!" unless value == send(attr_name)
|
13
|
+
instance_variable_set "@#{attr_name}", value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
define_attribute :id, :updated_at
|
19
|
+
|
20
|
+
def initialize(new_attributes={})
|
21
|
+
assign_attributes new_attributes
|
22
|
+
self.updated_at = 0
|
23
|
+
save # to clear all "changes"
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_key
|
27
|
+
"#{self.class.name.tableize}/#{id}-#{updated_at.to_i}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def commit
|
31
|
+
run_callbacks :commit
|
32
|
+
end
|
33
|
+
|
34
|
+
def save
|
35
|
+
if changes.any?
|
36
|
+
@previously_changed = changes
|
37
|
+
@changed_attributes.clear
|
38
|
+
commit
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def touch
|
45
|
+
self.updated_at += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def update_attributes(new_attributes={})
|
49
|
+
assign_attributes new_attributes
|
50
|
+
save
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def assign_attributes(new_attributes={})
|
56
|
+
new_attributes.each do |field, value|
|
57
|
+
send "#{field}=", value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: memcacheable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Scott McCormack
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-06-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activemodel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 4.0.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 4.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.13'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.13'
|
97
|
+
description: A Rails concern to make caching ActiveRecord objects as dead-simple as
|
98
|
+
possible. Uses the built-in Rails.cache mechanism, and implements the new finder
|
99
|
+
methods available in Rails 4.0 to ensure maximum future compatibility.
|
100
|
+
email:
|
101
|
+
- flintinatux@gmail.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- .gitignore
|
107
|
+
- .travis.yml
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- lib/memcacheable.rb
|
113
|
+
- lib/memcacheable/fetch_association.rb
|
114
|
+
- lib/memcacheable/fetch_belongs_to.rb
|
115
|
+
- lib/memcacheable/fetch_by.rb
|
116
|
+
- lib/memcacheable/fetch_by_criteria.rb
|
117
|
+
- lib/memcacheable/fetch_has_many.rb
|
118
|
+
- lib/memcacheable/fetch_has_one.rb
|
119
|
+
- lib/memcacheable/fetch_one.rb
|
120
|
+
- lib/memcacheable/fetch_where.rb
|
121
|
+
- lib/memcacheable/fetcher.rb
|
122
|
+
- lib/memcacheable/flusher.rb
|
123
|
+
- lib/memcacheable/version.rb
|
124
|
+
- memcacheable.gemspec
|
125
|
+
- spec/lib/memcacheable_spec.rb
|
126
|
+
- spec/spec_helper.rb
|
127
|
+
- spec/support/fake_model.rb
|
128
|
+
homepage: https://github.com/flintinatux/memcacheable
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.0.0.rc.2
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: A Rails concern to make caching ActiveRecord objects as dead-simple as possible.
|
152
|
+
Uses the built-in Rails.cache mechanism, and implements the new finder methods available
|
153
|
+
in Rails 4.0 to ensure maximum future compatibility.
|
154
|
+
test_files:
|
155
|
+
- spec/lib/memcacheable_spec.rb
|
156
|
+
- spec/spec_helper.rb
|
157
|
+
- spec/support/fake_model.rb
|