djmaze-arid_cache 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.rdoc +394 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/arid_cache.gemspec +104 -0
- data/init.rb +6 -0
- data/lib/arid_cache/active_record.rb +95 -0
- data/lib/arid_cache/cache_proxy.rb +368 -0
- data/lib/arid_cache/helpers.rb +86 -0
- data/lib/arid_cache/store.rb +125 -0
- data/lib/arid_cache.rb +47 -0
- data/rails/init.rb +1 -0
- data/spec/arid_cache/arid_cache_spec.rb +39 -0
- data/spec/arid_cache/cache_proxy_result_spec.rb +53 -0
- data/spec/arid_cache/cache_proxy_spec.rb +95 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/ar_query.rb +128 -0
- data/spec/support/custom_methods.rb +7 -0
- data/spec/support/matchers.rb +33 -0
- data/test/arid_cache_test.rb +414 -0
- data/test/console +15 -0
- data/test/lib/add_query_counting_to_active_record.rb +11 -0
- data/test/lib/blueprint.rb +29 -0
- data/test/lib/db_prepare.rb +34 -0
- data/test/lib/fix_active_support_file_store_expires_in.rb +18 -0
- data/test/lib/mock_rails.rb +29 -0
- data/test/lib/models/company.rb +6 -0
- data/test/lib/models/empty_user_relation.rb +5 -0
- data/test/lib/models/user.rb +32 -0
- data/test/log/.gitignore +0 -0
- data/test/test_helper.rb +19 -0
- metadata +177 -0
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
Gemfile.lock
|
21
|
+
.bundle/*
|
22
|
+
|
23
|
+
## PROJECT::SPECIFIC
|
24
|
+
test/**/*.log
|
25
|
+
test/tmp/*
|
26
|
+
tmp
|
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# A sample Gemfile
|
2
|
+
source "http://rubygems.org"
|
3
|
+
|
4
|
+
gem 'activerecord', '=2.3.8'
|
5
|
+
gem 'activesupport', '=2.3.8'
|
6
|
+
gem 'sqlite3-ruby', '=1.3.1'
|
7
|
+
|
8
|
+
gem 'will_paginate', '=2.3.15'
|
9
|
+
gem 'jeweler', '=1.4.0'
|
10
|
+
|
11
|
+
gem 'ruby-debug', '=0.10.3'
|
12
|
+
gem 'ruby-debug-base', '=0.10.3'
|
13
|
+
gem 'machinist', '=1.0.6'
|
14
|
+
gem 'faker', '=0.3.1'
|
15
|
+
gem 'rspec', '=1.3.0', :require => 'spec'
|
16
|
+
gem 'test-unit', '=1.2.3'
|
17
|
+
gem 'mocha', '=0.9.8'
|
18
|
+
gem 'arid_cache', :path => "./"
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Karl Varga <kjvarga@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
= AridCache
|
2
|
+
|
3
|
+
AridCache makes caching easy and effective. AridCache supports caching on all of your ActiveRecord model named scopes, class and instance methods right out of the box. AridCache keeps caching logic out of your model methods and clarifies your view code by making calls to cached result sets explicit.
|
4
|
+
|
5
|
+
AridCache supports caching large, expensive ActiveRecord collections by caching only the model IDs, provides efficient in-memory pagination of your cached collections, and gives you collection counts for free. Non-ActiveRecord collection data is cached unchanged allowing you to cache the results of any expensive operation simply by prepending your method call with <tt>cached_</tt>.
|
6
|
+
|
7
|
+
AridCache simplifies caching by supporting auto-expiring cache keys - as well as common options like <tt>:expires_in</tt> - and provides methods to help you manage your caches at the global, model class, model instance and per-cache level.
|
8
|
+
|
9
|
+
== Changes
|
10
|
+
|
11
|
+
v1.0.5: Support <tt>:raw</tt> and <tt>:clear</tt> options.
|
12
|
+
|
13
|
+
== Install
|
14
|
+
|
15
|
+
<b>Rails 3:</b>
|
16
|
+
|
17
|
+
Add the gem to your `Gemfile`
|
18
|
+
|
19
|
+
gem 'arid_cache'
|
20
|
+
|
21
|
+
Then
|
22
|
+
|
23
|
+
bundle install
|
24
|
+
|
25
|
+
For some reason AridCache is not being included into ActiveRecord, so add the following to an initializer to get around that until I fix it:
|
26
|
+
|
27
|
+
AridCache.init_rails
|
28
|
+
|
29
|
+
<b>Rails 2:</b>
|
30
|
+
|
31
|
+
Add the gem to your <tt>config/environment.rb</tt> file:
|
32
|
+
|
33
|
+
config.gem 'arid_cache'
|
34
|
+
|
35
|
+
Then
|
36
|
+
|
37
|
+
rake gems:install
|
38
|
+
|
39
|
+
== Introduction
|
40
|
+
|
41
|
+
The name AridCache comes from <b>A</b>ctive<b>R</b>ecord *ID* Cache. It's also very DRY...get it? :)
|
42
|
+
|
43
|
+
Out of the box AridCache supports caching on all your ActiveRecord class and instance methods and named scopes...basically if a class or class instance <tt>respond_to?</tt> something, you can cache it.
|
44
|
+
|
45
|
+
The way you interact with the cache via your model methods is to prepend the method call with <tt>cached_</tt>. The part of the method call after <tt>cached_</tt> serves as the basis for the cache key. For example,
|
46
|
+
|
47
|
+
User.cached_count # cache key is arid-cache-user-count
|
48
|
+
genre.cached_top_ten_tracks # cache key is arid-cache-genres/<id>-top_ten_tracks
|
49
|
+
|
50
|
+
You can also define caches that use compositions of methods or named scopes, or other complex queries, without having to add a new method to your class. This way you can also create different caches that all use the same method. For example,
|
51
|
+
|
52
|
+
# cache key is arid-cache-user-most_active_users
|
53
|
+
User.cached_most_active_users do
|
54
|
+
active.find(:order => 'activity DESC', :limit => 5)
|
55
|
+
end
|
56
|
+
|
57
|
+
=== ActiveRecord Collections
|
58
|
+
|
59
|
+
If the result of your <tt>cached_</tt> call is an array of ActiveRecords, AridCache only stores the IDs in the cache (because it's a bad idea to store records in the cache).
|
60
|
+
|
61
|
+
On subsequent calls we call <tt>find_all_by_id</tt> on the target class passing in the ActiveRecord IDs that were stored in the cache. AridCache will preserve the original ordering of your collection (you can change this using the <tt>:order</tt>).
|
62
|
+
|
63
|
+
The idea here is to cache collections that are expensive to query. Once the cache is loaded, retrieving the cached records from the database simply involves a <tt>SELECT * FROM table WHERE id IN (ids, ...)</tt>.
|
64
|
+
|
65
|
+
Consider how long it would take to get the top 10 favorited tracks of all time from a database with a million tracks and 100,000 users. Now compare that to selecting 10 tracks by ID from the track table. The performance gain is huge.
|
66
|
+
|
67
|
+
=== Base Types and Other Collections
|
68
|
+
|
69
|
+
Arrays of non-ActiveRecords are stored as-is so you can cache arrays of strings and other types without problems.
|
70
|
+
|
71
|
+
Any other objects (including single ActiveRecord objects) are cached and returned as-is.
|
72
|
+
|
73
|
+
=== Example
|
74
|
+
|
75
|
+
An example of caching using existing methods on your class:
|
76
|
+
|
77
|
+
class User < ActiveRecord::Base
|
78
|
+
has_many :pets
|
79
|
+
has_one :preferences
|
80
|
+
named_scope :active, :conditions => [ 'updated_at <= ', 5.minutes.ago ]
|
81
|
+
end
|
82
|
+
|
83
|
+
User.cached_count # uses the built-in count method
|
84
|
+
User.cached_active # only stores the IDs of the active users in the cache
|
85
|
+
User.cached_active_count # returns the count of active users directly from the cache
|
86
|
+
|
87
|
+
user.cached_pets_count # only selects the count until the collection is requested
|
88
|
+
user.cached_pets # loads the collection and stores the pets IDs in the cache
|
89
|
+
|
90
|
+
== Defining Your Caches
|
91
|
+
|
92
|
+
=== Dynamically
|
93
|
+
|
94
|
+
To dynamically define caches just pass a block to your <tt>cached_</tt> calls. Caches can be defined on your classes or class instances. For example,
|
95
|
+
|
96
|
+
User.cached_most_active_users do
|
97
|
+
active.find(:order => 'activity DESC', :limit => 5)
|
98
|
+
end
|
99
|
+
|
100
|
+
=> [#<User id: 23>, #<User id: 30>, #<User id: 5>, #<User id: 2>, #<User id: 101>]
|
101
|
+
|
102
|
+
user.cached_favorite_pets do
|
103
|
+
pets.find(:all, :conditions => { 'favorite' => true })
|
104
|
+
end
|
105
|
+
|
106
|
+
=> [#<Pet id: 11>, #<Pet id: 21>, #<Pet id: 3>]
|
107
|
+
|
108
|
+
=== Configuring Caches on your Models
|
109
|
+
|
110
|
+
We can clean up our views significantly by configuring caches on our model rather than defining them dynamically and passing options in each time. You configure caches by calling <tt>instance_caches(options={})</tt> or <tt>class_caches(options={})</tt> with a block and defining your caches inside the block (you don't need to prepend <tt>cached_</tt> when defining these caches because we are not returning results, just storing options).
|
111
|
+
|
112
|
+
You can pass a hash of options to <tt>instance_caches</tt> and <tt>class_caches</tt> to have those options applied to all caches in the block. The following is a more complex example that also demonstrates nested cached calls.
|
113
|
+
|
114
|
+
# app/models/genre.rb
|
115
|
+
class Genre
|
116
|
+
class_caches do
|
117
|
+
most_popular do
|
118
|
+
popular(:limit => 10, :order => 'popularity DESC')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
instance_caches(:order => 'release_date DESC') do
|
123
|
+
highlight_tracks(:include => [:album, :artist]) do
|
124
|
+
cached_tracks(:limit => 10, :include => [:album, :artist])
|
125
|
+
end
|
126
|
+
highlight_artists(:order => nil) do # override the global :order option
|
127
|
+
cached_artists(:limit => 10)
|
128
|
+
end
|
129
|
+
highlight_albums(:include => :artist) do
|
130
|
+
cached_albums(:limit => 3, :include => :artist)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# app/controllers/genre_controller.rb
|
136
|
+
@most_popular = Genre.cached_most_popular
|
137
|
+
@tracks = @genre.cached_highlight_tracks
|
138
|
+
@artists = @genre.cached_highlight_artists
|
139
|
+
@albums = @genre.cached_highlight_albums
|
140
|
+
|
141
|
+
You can configure your caches in this manner wherever you want, but I think the model is a good place. If you wanted to move all your cache configurations to a file in <tt>lib</tt> or elsewhere, your calls would look like,
|
142
|
+
|
143
|
+
Genre.class_caches do
|
144
|
+
...
|
145
|
+
end
|
146
|
+
Genre.instance_caches do
|
147
|
+
...
|
148
|
+
end
|
149
|
+
|
150
|
+
== Cache Keys
|
151
|
+
|
152
|
+
AridCache cache keys are defined based on the methods you call to interact with the cache. For example:
|
153
|
+
|
154
|
+
Album.cached_featured_albums => cache key is arid-cache-album-featured_albums
|
155
|
+
album.cached_top_tracks => cache key is arid-cache-albums/<id>-top_tracks
|
156
|
+
|
157
|
+
Caches on model instances can be set to automatically incorporate the ActiveRecord <tt>cache_key</tt> which includes the <tt>updated_at</tt> timestamp of that instance, making them auto-expire when the instance is updated.
|
158
|
+
|
159
|
+
To incorporate the the <tt>cache_key</tt> pass <b><tt>:auto_expire => true</tt></b> to your cache method:
|
160
|
+
|
161
|
+
album.cached_top_tracks(:auto_expire => true) => cache key like arid-cache-albums/2-20091211120100-top_tracks
|
162
|
+
|
163
|
+
Or via the cache configuration:
|
164
|
+
|
165
|
+
Album.instance_caches do
|
166
|
+
top_tracks(:auto_expire => true)
|
167
|
+
end
|
168
|
+
|
169
|
+
If you need to examine values in the cache yourself you can build the AridCache key by calling <tt>arid_cache_key('method')</tt> on your object, whether it is a class or instance. Using the examples above we would call,
|
170
|
+
|
171
|
+
Album.arid_cache_key('featured_albums') => arid-cache-album-featured_albums
|
172
|
+
album.arid_cache_key('top_tracks') => arid-cache-albums/2-top_tracks
|
173
|
+
album.arid_cache_key('top_tracks', :auto_expire => true) => arid-cache-albums/2-20091211120100-top_tracks
|
174
|
+
|
175
|
+
== Managing your Caches
|
176
|
+
|
177
|
+
=== Deleting & Expiring Caches
|
178
|
+
|
179
|
+
AridCache provides methods to help you clear your caches:
|
180
|
+
|
181
|
+
AridCache.clear_caches => expires all AridCache caches
|
182
|
+
Model.clear_caches => expires class and instance-level caches for this model
|
183
|
+
Model.clear_instance_caches => expires instance-level caches for this model
|
184
|
+
Model.clear_class_caches => expires class-level caches for this model
|
185
|
+
|
186
|
+
The <tt>Model.clear_caches</tt> methods are also available on all model instances.
|
187
|
+
|
188
|
+
<B>Your cache store needs to support the <tt>delete_matched</tt> method for the above to work. Currently MemCacheStore and MemoryStore do not.</b>
|
189
|
+
|
190
|
+
Alternatively you can pass a <b><tt>:force => true</tt></b> option in your <tt>cached_</tt> calls to force a refresh of a particular cache, while still returning the refreshed results. For example:
|
191
|
+
|
192
|
+
Album.cached_featured_albums(:force => true) => returns featured albums
|
193
|
+
album.cached_top_tracks(:force => true) => returns top tracks
|
194
|
+
|
195
|
+
If you just want to clear a cache without forcing a refresh pass <b><tt>:clear => true</tt></b>. The cached value will be deleted with no unnecessary queries or cache reads being performed. It is safe to pass this option even if there is nothing in the cache yet. The method returns the result of calling <tt>delete</tt> on your cache object. For example:
|
196
|
+
|
197
|
+
Album.cached_featured_albums(:clear => true) => returns false
|
198
|
+
Rails.cache.read(Album.arid_cache_key(:featured_albums)) => returns nil
|
199
|
+
|
200
|
+
You can pass an <b><tt>:expires_in</tt></b> option to your caches to manage your cache expiry (if your cache store supports this option, which most do).
|
201
|
+
|
202
|
+
Album.cached_featured_albums(:expires_in => 1.day)
|
203
|
+
album.cached_top_tracks(:expires_in => 1.day)
|
204
|
+
|
205
|
+
Or via the cache configuration,
|
206
|
+
|
207
|
+
Album.instance_caches(:expires_in => 1.day) do
|
208
|
+
top_tracks
|
209
|
+
featured_albums
|
210
|
+
end
|
211
|
+
|
212
|
+
If you would like to be able to pass more options to your cache store (like <tt>:unless_exists</tt>, etc), just add them to the <tt>AridCache::CacheProxy::OPTIONS_FOR_CACHE</tt> class constant, for example
|
213
|
+
|
214
|
+
AridCache::CacheProxy::OPTIONS_FOR_CACHE.push(:raw, :unless_exist)
|
215
|
+
|
216
|
+
== Extras
|
217
|
+
|
218
|
+
=== Cached Counts
|
219
|
+
|
220
|
+
AridCache gives you counts for free. When a collection is stored in the cache
|
221
|
+
AridCache stores the count as well so the next time you request the count it
|
222
|
+
just takes a single read from the cache.
|
223
|
+
|
224
|
+
To get the count just append <tt>_count</tt> to your <tt>cached_</tt> call. For example, if we have a cache like <tt>album.cached_tracks</tt> we can get the count by calling,
|
225
|
+
|
226
|
+
album.cached_tracks => returns an array of tracks
|
227
|
+
album.cached_tracks_count => returns the count with a single read from the cache
|
228
|
+
|
229
|
+
This is also supported for your non-ActiveRecord collections if the collection <tt>responds_to?(:count)</tt>. For example,
|
230
|
+
|
231
|
+
album.cached_similar_genres => returns ['Pop', 'Rock', 'Rockabilly']
|
232
|
+
album.cached_similar_genres_count => returns 3
|
233
|
+
|
234
|
+
Sometimes you may want the collection count without loading and caching the collection itself. AridCache is smart enough that if you only ask for a count it will only query for the count. This is only possible if the return value of your method is a named scope or association proxy (since these are lazy-loaded unlike a call to <tt>find()</tt>).
|
235
|
+
|
236
|
+
In the example above if we only ever call <tt>album.cached_tracks_count</tt>, only the count will be cached. If we subsequently call <tt>album.cached_tracks</tt> the collection will be loaded and the IDs cached as per normal.
|
237
|
+
|
238
|
+
Other methods for caching counts are provided for us by virtue of ActiveRecord's built-in methods and named scopes, for example,
|
239
|
+
|
240
|
+
Artist.cached_count # takes advantage of the built-in method Artist.count
|
241
|
+
|
242
|
+
=== Pagination
|
243
|
+
|
244
|
+
AridCache supports pagination using WillPaginate. If you are not changing the order of the cached collection the IDs are paginated in memory and only that page is selected from the database - directly from the target table, which is extremely fast.
|
245
|
+
|
246
|
+
An advantage of using AridCache is that since we already have the size of the collection in the cache no query is required to set the <tt>:total_entries</tt> on the <tt>WillPaginate::Collection</tt>.
|
247
|
+
|
248
|
+
To paginate just pass a <tt>:page</tt> option in your call to <tt>cached_</tt>. If you don't pass a value for <tt>:per_page</tt> AridCache gets the value from <tt>Model.per_page</tt>, which is what <tt>WillPaginate</tt> uses.
|
249
|
+
|
250
|
+
The supported pagination options are:
|
251
|
+
:page, :per_page, :total_entries, :finder
|
252
|
+
|
253
|
+
Some examples of pagination:
|
254
|
+
|
255
|
+
User.cached_active(:page => 1, :per_page => 30)
|
256
|
+
User.cached_active(:page => 2) # uses User.per_page
|
257
|
+
user.cached_pets(:page => 1) # uses Pet.per_page
|
258
|
+
|
259
|
+
If you want to paginate using a different ordering, pass an <tt>:order</tt> option. Because the order is being changed AridCache cannot paginate in memory. Instead, the cached IDs are passed to your <tt>Model.paginate</tt> method along with any other options and the database will order the collection, apply limits and offsets, etc. Because the number of records the database deals with is limited, this is still much, much faster than ordering over the whole table.
|
260
|
+
|
261
|
+
For example, the following queries will work:
|
262
|
+
|
263
|
+
user.cached_companies(:page => 1, :per_page => 3, :order => 'name DESC')
|
264
|
+
user.cached_companies(:page => 1, :per_page => 3, :order => 'name ASC')
|
265
|
+
|
266
|
+
By specifying an <tt>:order</tt> option in our cached call we can get different "views" of the cached collection. I think this a "good thing". However, you need to be aware that in order to guarantee that the ordering you requested is the same as the order of the initial results (when the cache was primed), we have to order in the database. This results in two queries being executed the first time you query the cache (one to prime it and the other to order and return the results). If no order option is specified, we can skip the second query and do everything in memory.
|
267
|
+
|
268
|
+
If you have an expensive cache and don't want that extra query, just define a new cache with your desired ordering and use that. Make sure that the order of the initial results matches your desired ordering. Building on the example above we could do:
|
269
|
+
|
270
|
+
User.instance_caches do
|
271
|
+
companies_asc do
|
272
|
+
companies(:order => 'name ASC')
|
273
|
+
end
|
274
|
+
companies_desc do
|
275
|
+
companies(:order => 'name DESC')
|
276
|
+
end
|
277
|
+
end
|
278
|
+
user.cached_companies_asc(:page => 1, :per_page => 3)
|
279
|
+
user.cached_companies_desc(:page => 1, :per_page => 3)
|
280
|
+
|
281
|
+
|
282
|
+
=== Limit & Offset
|
283
|
+
|
284
|
+
You apply <tt>:limit</tt> and <tt>:offset</tt> options in a similar manner to the <tt>:page</tt> and <tt>:per_page</tt> options. The limit and offset will be applied in memory and only the resulting subset selected from the target table - unless you specify a new order.
|
285
|
+
|
286
|
+
user.cached_pets(:limit => 2, :include => :toys)
|
287
|
+
user.cached_pets(:limit => 2, :offset => 3, :include => :toys)
|
288
|
+
genre.cached_top_ten_tracks { cached_tracks(:limit => 10, :order => 'popularity DESC') }
|
289
|
+
|
290
|
+
=== Other Options to <tt>find</tt>
|
291
|
+
|
292
|
+
The supported options to <tt>find</tt> are:
|
293
|
+
:conditions, :include, :joins, :limit, :offset, :order,
|
294
|
+
:select, :readonly, :group, :having, :from, :lock
|
295
|
+
|
296
|
+
You can pass options like <tt>:include</tt> (or any other valid <tt>find</tt> options) to augment the results of your cached query. Just because all of the options are supported, does not mean it's a good idea to use them, though. Take a look at your logs to see how AridCache is interacting with the cache and the database if you don't get the results you expect.
|
297
|
+
|
298
|
+
For example, we could call:
|
299
|
+
|
300
|
+
User.cached_active(:page => 2, :per_page => 10, :include => :preferences)
|
301
|
+
|
302
|
+
To return page two of the active users, with the <tt>preferences</tt> association eager-loaded for all the users.
|
303
|
+
|
304
|
+
=== Accessing the cached IDs directly
|
305
|
+
|
306
|
+
Sometimes you may want to access the cached list of record IDs without instantiating all the records. This can be useful, for example, to determine if a particular track belongs to a user's favorite tracks. If we have cached the list of favorite tracks, we just need to determine whether the track's ID appears in the cached list of IDs.
|
307
|
+
|
308
|
+
The cached result is a <tt>AridCache::CacheProxy::Result</tt> and can be accessed by passing the <b><tt>:raw => true</tt></b> option in your cached call. The <tt>AridCache::CacheProxy::Result</tt> is a type of <tt>Struct</tt> with methods to return the <tt>ids</tt>, <tt>count</tt> and <tt>klass</tt> of the cached records.
|
309
|
+
|
310
|
+
Note that passing the <tt>:raw</tt> option to your cache store is not supported, because the AridCache option shares the same name. If you really want to get the marshalled result from your cache you will have to use <tt>cache.read</tt>, manually passing in the AridCache key and <tt>:raw</tt> option.
|
311
|
+
|
312
|
+
Usage example:
|
313
|
+
|
314
|
+
user = User.first
|
315
|
+
user.cached_favorite_tracks => returns [#<Track:1>, #<Track:2>]
|
316
|
+
user.cached_favorite_tracks(:raw => true) => returns
|
317
|
+
{
|
318
|
+
:klass => "Track", # stored as a string
|
319
|
+
:count => 1,
|
320
|
+
:ids => [1, 2]
|
321
|
+
}
|
322
|
+
user.cached_favorite_tracks(:raw => true).ids => returns [1, 2]
|
323
|
+
|
324
|
+
The cache will be primed if it is empty, so you can be sure that it will always return a <tt>AridCache::CacheProxy::Result</tt>.
|
325
|
+
|
326
|
+
In some circumstances - like when you are querying on a named scope - if you have only requested a count, only the count is computed, which means the ids array is <tt>nil</tt>. When you call your cached method passing in <tt>:raw => true</tt> AridCache detects that the ids array has not yet been set, so in this case it will perform a query to seed the ids array before returning the result. This can be seen in the following example:
|
327
|
+
|
328
|
+
class User
|
329
|
+
named_scope :guests, :conditions => { :account_type => ['guest'] }
|
330
|
+
end
|
331
|
+
|
332
|
+
User.cached_guests_count => returns 4
|
333
|
+
Rails.cache.read(User.arid_cache_key(:guests)) => returns
|
334
|
+
{
|
335
|
+
:klass => "User",
|
336
|
+
:count => 4,
|
337
|
+
:ids => nil # notice the ids array is nil in the cache
|
338
|
+
}
|
339
|
+
User.cached_guests(:raw => true) => returns
|
340
|
+
{
|
341
|
+
:klass => "User",
|
342
|
+
:count => 4,
|
343
|
+
:ids => [2, 235, 236, 237] # the ids array is seeded before returning
|
344
|
+
}
|
345
|
+
|
346
|
+
== Efficiency
|
347
|
+
|
348
|
+
* AridCache intercepts calls to <tt>cached_</tt> methods using <tt>method_missing</tt> then defines those methods on your models as they are called, so they bypass method missing on subsequent calls.
|
349
|
+
* In-memory pagination of cached collections speeds up your queries. See _Pagination_.
|
350
|
+
* If you only request a count AridCache will only select the count. See <i>Cached Counts</i>.
|
351
|
+
* If a collection has already been loaded, you get the count for free. See <i>Cached Counts</i>.
|
352
|
+
|
353
|
+
== Compatibility
|
354
|
+
|
355
|
+
Tested on Ruby 1.8.6, 1.8.7, REE 1.8.7 and 1.9.1.
|
356
|
+
Tested in Rails 2.3.* and Rails 3
|
357
|
+
|
358
|
+
For Ruby < 1.8.7 you probably want to include the following to extend the Array class with a <tt>count</tt> method. Otherwise your <tt>cached_<key>_count</tt> calls probably won't work:
|
359
|
+
|
360
|
+
Array.class_eval { alias count size }
|
361
|
+
|
362
|
+
For Rails 3 for some reason AridCache is not being included into ActiveRecord, so add the following to an initializer to get around that:
|
363
|
+
|
364
|
+
AridCache.init_rails
|
365
|
+
|
366
|
+
== Resources & Metrics
|
367
|
+
|
368
|
+
|
369
|
+
* {RDoc}[http://rdoc.info/projects/kjvarga/arid_cache]
|
370
|
+
* {GetCaliper Metrics}[http://getcaliper.com/caliper/project?repo=git%3A%2F%2Fgithub.com%2Fkjvarga%2Farid_cache.git]
|
371
|
+
|
372
|
+
== Known Issues
|
373
|
+
|
374
|
+
1. <b>Caches that contains duplicate records will only return unique records on subsequent calls</b>. This is because of the way <tt>find</tt> works when selecting multiple ids. For example, if your query returns <tt>[#<User id: 1>, #<User id: 1>, #<User id: 1>]</tt>, the IDs are cached as <tt>[1,1,1]</tt>. On the next call to the cache we load the IDs using <tt>User.find_all_by_id([1,1,1])</tt> which returns <tt>[#<User id: 1>]</tt>, not <tt>[#<User id: 1>, #<User id: 1>, #<User id: 1>]</tt> as you might have expected.
|
375
|
+
2. <b>You can't cache polymorphic arrays</b> e.g. [#<User id: 1>, #<Pet id: 5>] because it expects all ActiveRecords to be of the same class. We could accept a <tt>:polymorphic => true</tt> option but I don't think this is a great idea because instantiating all the records would result in a lot of queries to the individual tables.
|
376
|
+
|
377
|
+
== Contributors
|
378
|
+
|
379
|
+
Contributions are welcome! Please,
|
380
|
+
|
381
|
+
* Fork the project.
|
382
|
+
* Make your feature addition or bug fix.
|
383
|
+
* Add tests for it (this is important so I don't break it in a future release).
|
384
|
+
* Commit (don't mess with the Rakefile, version, or history).
|
385
|
+
* Send me a pull request.
|
386
|
+
|
387
|
+
|
388
|
+
==== Thank-you to these contributors to AridCache:
|
389
|
+
|
390
|
+
* {Sutto}[http://github.com/Sutto]
|
391
|
+
|
392
|
+
== Copyright
|
393
|
+
|
394
|
+
Copyright (c) 2009 Karl Varga. See LICENSE for details.
|