ca_ching 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +16 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +106 -0
- data/Rakefile +19 -0
- data/SPEC.md +109 -0
- data/ca_ching.gemspec +25 -0
- data/lib/ca_ching/adapters/active_record.rb +23 -0
- data/lib/ca_ching/adapters/redis.rb +127 -0
- data/lib/ca_ching/configuration.rb +23 -0
- data/lib/ca_ching/core_ext/array.rb +7 -0
- data/lib/ca_ching/errors.rb +3 -0
- data/lib/ca_ching/index.rb +28 -0
- data/lib/ca_ching/query/abstract.rb +106 -0
- data/lib/ca_ching/query/calculation.rb +6 -0
- data/lib/ca_ching/query/select.rb +6 -0
- data/lib/ca_ching/read_through.rb +64 -0
- data/lib/ca_ching/version.rb +13 -0
- data/lib/ca_ching/write_through.rb +23 -0
- data/lib/ca_ching.rb +21 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/people.rb +5 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/ca_ching/adapters/active_record_spec.rb +5 -0
- data/spec/ca_ching/adapters/redis_spec.rb +176 -0
- data/spec/ca_ching/configuration_spec.rb +7 -0
- data/spec/ca_ching/index_spec.rb +22 -0
- data/spec/ca_ching/query/abstract_spec.rb +195 -0
- data/spec/ca_ching/query/calculation_spec.rb +9 -0
- data/spec/ca_ching/query/select_spec.rb +9 -0
- data/spec/ca_ching/read_through_spec.rb +214 -0
- data/spec/ca_ching/write_through_spec.rb +25 -0
- data/spec/console.rb +22 -0
- data/spec/helpers/ca_ching_helper.rb +2 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/schema.rb +98 -0
- metadata +150 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
guard 'rspec', :version => 2, :cli => "--color --format documentation" do
|
2
|
+
watch(%r{^spec/ca_ching/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/ca_ching/(.+)\.rb$}) { |m| "spec/ca_ching/#{m[1]}_spec.rb" }
|
4
|
+
watch(%r{^spec/ca_ching/adapters/.+_spec\.rb$})
|
5
|
+
watch(%r{^lib/ca_ching/adapters/(.+)\.rb$}) { |m| "spec/ca_ching/adapters/#{m[1]}_spec.rb" }
|
6
|
+
watch(%r{^spec/ca_ching/query/.+_spec\.rb$})
|
7
|
+
watch(%r{^lib/ca_ching/query/(.+)\.rb$}) { |m| "spec/ca_ching/query/#{m[1]}_spec.rb" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Andrew Latimer
|
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.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# CaChing
|
2
|
+
|
3
|
+
CaChing is a write-through and read-through caching library for ActiveRecord 3.1+.
|
4
|
+
|
5
|
+
That means that when you read from the database, your results are stored in the cache (read-through). When you write to
|
6
|
+
the database, whatever is written to the database is also written to the cache (write-through). If the results are already
|
7
|
+
in the cache, great, they're read straight from there on a read, and updated on a write.
|
8
|
+
|
9
|
+
Take a look at `SPEC.md` for what's planned for v1.
|
10
|
+
|
11
|
+
## Getting started
|
12
|
+
|
13
|
+
In your Gemfile:
|
14
|
+
|
15
|
+
gem 'ca_ching'
|
16
|
+
# gem 'ca_ching', :git => 'git://github.com/ahlatimer/ca_ching.git' # Track git repo
|
17
|
+
|
18
|
+
In an initializer:
|
19
|
+
|
20
|
+
CaChing.configure do |config|
|
21
|
+
config.cache = CaChing::Adapters::Redis.new
|
22
|
+
config.enabled = true # defaults to true
|
23
|
+
end
|
24
|
+
|
25
|
+
## Defining what to cache
|
26
|
+
|
27
|
+
The simplest case is to add `index :field_name` to your model. From then on, any query that does a find based on
|
28
|
+
that field will get cached.
|
29
|
+
|
30
|
+
class Person < ActiveRecord::Base
|
31
|
+
index :id
|
32
|
+
index :first_name
|
33
|
+
index :last_name
|
34
|
+
|
35
|
+
has_many :addresses
|
36
|
+
end
|
37
|
+
|
38
|
+
class Address < ActiveRecord::Base
|
39
|
+
# Let's you find by associations (person.addresses.all)
|
40
|
+
index :person_id
|
41
|
+
|
42
|
+
belongs_to :person
|
43
|
+
end
|
44
|
+
|
45
|
+
## Using the cache
|
46
|
+
|
47
|
+
If you've defined your indices, everything should work without any additional effort. Doing
|
48
|
+
`Person.where(:first_name => 'Andrew')` should hit the cache and return what is there, or
|
49
|
+
miss and pull the data into the cache.
|
50
|
+
|
51
|
+
## Queries supported
|
52
|
+
|
53
|
+
Generally anything `eqality` is supported by CaChing. Currently, only single fields can be found (e.g., `Person.where(:name => 'Andrew')` will be cached,
|
54
|
+
`Person.where(:name => 'Andrew', :age => 22)` is not). There is some plumbing for adding support for the latter, and support will be
|
55
|
+
introduced as soon as possible.
|
56
|
+
|
57
|
+
Anything including joins, includes, `OR`, and inequality (!=) are not supported, nor do I have any plans for adding support.
|
58
|
+
|
59
|
+
For example, these queries are supported:
|
60
|
+
|
61
|
+
Person.where(:first_name => 'Andrew')
|
62
|
+
Person.find_by_first_name('Andrew')
|
63
|
+
Person.find_all_by_first_name('Andrew')
|
64
|
+
|
65
|
+
These queries are not:
|
66
|
+
|
67
|
+
Person.where('first_name = ? OR first_name = ?', 'Andrew', 'David')
|
68
|
+
Person.where('first_name != ?', 'Andrew')
|
69
|
+
Person.where(:first_name => 'Andrew').order('created_at DESC') # not supported because first_name isn't sorted by anything
|
70
|
+
Person.where('id >= 200')
|
71
|
+
Person.where('created_at >= ?', Date.today - 1).where(:first_name => 'Andrew')
|
72
|
+
Person.where('created_at <= ?', Date.today - 1).where(:first_name => 'Andrew').limit(10).order('created_at desc')
|
73
|
+
|
74
|
+
## Caveats
|
75
|
+
|
76
|
+
### Order
|
77
|
+
|
78
|
+
Order is not currently supported, although there will be some order semantics in the next version.
|
79
|
+
|
80
|
+
### Composite keys and queries against multiple fields
|
81
|
+
|
82
|
+
Because of the awesomeness from Redis, composite keys aren't necessary for queries with multiple fields.
|
83
|
+
If all of the fields are indexed, CaChing will try to use the cache to build the results.
|
84
|
+
|
85
|
+
Redis requires that sorted set intersections be stored in a resulting set. If the composite key is not specified
|
86
|
+
for a query against those fields, that resulting set will be destroyed as soon as the results are read. If the
|
87
|
+
composite key *is* specified, CaChing will keep the resulting set.
|
88
|
+
|
89
|
+
While there is support for this in the Redis adapter, there isn't any support in the ActiveRecord ties as I haven't
|
90
|
+
decided how, exactly, I'd like to add this.
|
91
|
+
|
92
|
+
## Ruby/Rails versions supported
|
93
|
+
|
94
|
+
Ruby 1.9.2 and Rails 3.1+ are officially supported. I try to stick to Ruby 1.8.7 syntax, so it may be supported,
|
95
|
+
but use it with the understanding that you are doing so at your own risk.
|
96
|
+
|
97
|
+
## Patches and Issues
|
98
|
+
|
99
|
+
I'd love the help! If you find an issue, please report it on the [Github issues page](http://github.com/ahlatimer/ca_ching/issues).
|
100
|
+
If you fix an issue, please include a spec that illustrates the issue. If you submit a feature, include thorough specs.
|
101
|
+
Don't bump up the version in your pull request. If you want to keep different versions in your fork, feel free, but
|
102
|
+
please do not include them in your pull request.
|
103
|
+
|
104
|
+
## Acknowledgments
|
105
|
+
|
106
|
+
Inspired by (and a bit of code copied from) [Cache Money](http://github.com/ngmoco/cache-money).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |rspec|
|
7
|
+
rspec.rspec_opts = ['--debugger', '--color', '--format documentation']
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :spec
|
11
|
+
|
12
|
+
desc "Open an irb session with CaChing and the sample data used in specs"
|
13
|
+
task :console do
|
14
|
+
require 'irb'
|
15
|
+
require 'irb/completion'
|
16
|
+
require 'console'
|
17
|
+
ARGV.clear
|
18
|
+
IRB.start
|
19
|
+
end
|
data/SPEC.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# CaChing
|
2
|
+
|
3
|
+
CaChing is a write-through and read-through caching library for ActiveRecord 3.1+.
|
4
|
+
|
5
|
+
That means that when you read from the database, your results are stored in the cache (read-through). When you write to
|
6
|
+
the database, whatever is written to the database is also written to the cache (write-through). If the results are already
|
7
|
+
in the cache, great, they're read straight from there on a read, and updated on a write.
|
8
|
+
|
9
|
+
## Getting started
|
10
|
+
|
11
|
+
In your Gemfile:
|
12
|
+
|
13
|
+
gem 'ca_ching'
|
14
|
+
# gem 'ca_ching', :git => 'git://github.com/ahlatimer/ca_ching.git' # Track git repo
|
15
|
+
|
16
|
+
In an initializer:
|
17
|
+
|
18
|
+
CaChing.configure do |config|
|
19
|
+
# some configuration options go here, likely just for redis...
|
20
|
+
end
|
21
|
+
|
22
|
+
## Defining what to cache
|
23
|
+
|
24
|
+
The simplest case is to add `index :field_name` to your model. From then on, any query that does a find based on
|
25
|
+
that field will get cached. You can also specify the maximum number of elements stored, the TTL (time to live),
|
26
|
+
and the order.
|
27
|
+
|
28
|
+
class Person < ActiveRecord::Base
|
29
|
+
index :id
|
30
|
+
index :first_name, :limit => 1000
|
31
|
+
index :last_name, :limit => 1000, :order => { :created_at => :desc }
|
32
|
+
|
33
|
+
has_many :addresses
|
34
|
+
end
|
35
|
+
|
36
|
+
class Address < ActiveRecord::Base
|
37
|
+
# You can have associations, so person.addresses.find(1) will hit the cache if a composite index is specified
|
38
|
+
index [:id, :person_id], :limit => 100, :ttl => 10.minutes
|
39
|
+
# Or just find all of them based on person_id (person.addresses.all)
|
40
|
+
index :person_id
|
41
|
+
|
42
|
+
belongs_to :person
|
43
|
+
end
|
44
|
+
|
45
|
+
## Using the cache
|
46
|
+
|
47
|
+
If you've defined your indices, everything should work without any additional effort. Doing
|
48
|
+
`Person.where(:first_name => 'Andrew')` should hit the cache and return what is there, or
|
49
|
+
miss and pull the data into the cache.
|
50
|
+
|
51
|
+
## Queries supported
|
52
|
+
|
53
|
+
Generally anything with an `AND` and/or `eqality` is supported by CaChing. Anything including
|
54
|
+
joins, includes, `OR`, and inequality (!=) are not supported at this time. Queries involving
|
55
|
+
comparators (>, <, >=, <=) are supported ONLY if the order is specified. The caveats for order
|
56
|
+
(covered below) apply here as well.
|
57
|
+
|
58
|
+
For example, these queries are supported:
|
59
|
+
|
60
|
+
Person.where(:first_name => 'Andrew')
|
61
|
+
Person.find_by_first_name('Andrew')
|
62
|
+
Person.where('created_at >= ?', Date.today - 1).where(:first_name => 'Andrew')
|
63
|
+
Person.where('created_at <= ?', Date.today - 1).where(:first_name => 'Andrew').limit(10).order('created_at desc')
|
64
|
+
|
65
|
+
These queries are not:
|
66
|
+
|
67
|
+
Person.where('first_name = ? OR first_name = ?', 'Andrew', 'David')
|
68
|
+
Person.where('first_name != ?', 'Andrew')
|
69
|
+
Person.where(:first_name => 'Andrew').order('created_at DESC') # not supported because first_name isn't sorted by anything
|
70
|
+
Person.where('id >= 200')
|
71
|
+
|
72
|
+
## Caveats
|
73
|
+
|
74
|
+
### Order
|
75
|
+
|
76
|
+
If the order in the query is not the same as the order in the index directive, the cache will be skipped.
|
77
|
+
It's likely faster to take the DB hit than to try to sort in Ruby.
|
78
|
+
|
79
|
+
If multiple fields are specified, at least one of them must have the order of the query.
|
80
|
+
|
81
|
+
If the field is sorted, it must respond to `to_f` and return a reasonable response (e.g., even though a
|
82
|
+
string will respond to `to_f`, it will return `0.0` if it is not a number).
|
83
|
+
|
84
|
+
By default, indexes are ordered by the primary key.
|
85
|
+
|
86
|
+
### Composite keys and queries against multiple fields
|
87
|
+
|
88
|
+
Because of the awesomeness from Redis, composite keys aren't necessary for queries with multiple fields.
|
89
|
+
If all of the fields are indexed, CaChing will try to use the cache to build the results.
|
90
|
+
|
91
|
+
Redis requires that sorted set intersections be stored in a resulting set. If the composite key is not specified
|
92
|
+
for a query against those fields, that resulting set will be destroyed as soon as the results are read. If the
|
93
|
+
composite key *is* specified, CaChing will keep the resulting set.
|
94
|
+
|
95
|
+
## Ruby/Rails versions supported
|
96
|
+
|
97
|
+
Ruby 1.9.2 and Rails 3.1+ are officially supported. I try to stick to Ruby 1.8.7 syntax, so it may be supported,
|
98
|
+
but use it with the understanding that you are doing so at your own risk.
|
99
|
+
|
100
|
+
## Patches and Issues
|
101
|
+
|
102
|
+
I'd love the help! If you find an issue, please report it on the [Github issues page](http://github.com/ahlatimer/ca_ching/issues).
|
103
|
+
If you fix an issue, please include a spec that illustrates the issue. If you submit a feature, include thorough specs.
|
104
|
+
Don't bump up the version in your pull request. If you want to keep different versions in your fork, feel free, but
|
105
|
+
please do not include them in your pull request.
|
106
|
+
|
107
|
+
## Acknowledgments
|
108
|
+
|
109
|
+
Inspired by (and a bit of code copied from) [Cache Money](http://github.com/ngmoco/cache-money).
|
data/ca_ching.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ca_ching/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ca_ching"
|
7
|
+
s.version = CaChing::Version.to_s
|
8
|
+
s.authors = ["Andrew Latimer"]
|
9
|
+
s.email = ["andrew@elpasoera.com"]
|
10
|
+
s.homepage = "http://github.com/ahlatimer/ca_ching"
|
11
|
+
s.summary = %q{Write-through ActiveRecord model caching that's right on the money}
|
12
|
+
s.description = %q{Write-through ActiveRecord model caching that's right on the money}
|
13
|
+
|
14
|
+
s.rubyforge_project = "ca_ching"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'hiredis'
|
22
|
+
s.add_dependency 'redis', '2.2.2'
|
23
|
+
s.add_dependency 'activesupport', '>= 3.1.0'
|
24
|
+
s.add_dependency 'activerecord', '>= 3.1.0'
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
ActiveRecord::Base.send :include, CaChing::Index
|
3
|
+
ActiveRecord::Base.send :include, CaChing::WriteThrough
|
4
|
+
ActiveRecord::Relation.send :include, CaChing::ReadThrough
|
5
|
+
|
6
|
+
class ActiveRecord::Base
|
7
|
+
attr_accessor :from_cache
|
8
|
+
|
9
|
+
def from_cache?
|
10
|
+
@from_cache ||= false
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_keys(options={})
|
14
|
+
was = options[:old_values] ? "_was" : ""
|
15
|
+
indexed_fields.map do |index, options|
|
16
|
+
if index.is_a?(Array)
|
17
|
+
self.class.table_name.to_s + index.map { |index| "#{index}=#{self.send("#{index}#{was}")}" }.join("&")
|
18
|
+
else
|
19
|
+
"#{self.class.table_name}:#{index}=#{self.send("#{index}#{was}")}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module CaChing
|
2
|
+
module Adapters
|
3
|
+
class Redis
|
4
|
+
def initialize(options={})
|
5
|
+
@cache = ::Redis.new(options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def find(query, options={})
|
9
|
+
results = nil
|
10
|
+
# where_values in the form { :field => ['=', 'value'] }
|
11
|
+
where_values = query.where
|
12
|
+
keys = where_values.map { |where_value| "#{where_value[0]}#{where_value[1][0]}#{where_value[1][1]}" }
|
13
|
+
return nil if keys.empty?
|
14
|
+
|
15
|
+
offset = query.offset || 0
|
16
|
+
limit = (query.limit || 0) + offset - 1
|
17
|
+
|
18
|
+
if keys.length <= 1
|
19
|
+
key = "#{query.table_name}:#{keys[0]}"
|
20
|
+
return nil unless @cache.exists key # the key has never been added to the cache, so it's a miss
|
21
|
+
|
22
|
+
results = @cache.zrange(key, offset, limit)
|
23
|
+
else # needs an intersection
|
24
|
+
intersection_key = "#{query.table_name}:#{keys.join "&"}"
|
25
|
+
|
26
|
+
if @cache.exists intersection_key
|
27
|
+
results = @cache.zrange(intersection_key, offset, limit)
|
28
|
+
else
|
29
|
+
return nil unless keys.inject(true) { |memo, key| @cache.exists("#{query.table_name}:#{key}") && memo }
|
30
|
+
@cache.zinterstore intersection_key, *keys.map { |key| "#{query.table_name}:#{key}" }
|
31
|
+
@cache.zrange(intersection_key, offset, limit)
|
32
|
+
@cache.del(intersection_key) unless options[:keep_intersection]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
inflate(results, :for => query)
|
37
|
+
end
|
38
|
+
|
39
|
+
def insert(objects, options={})
|
40
|
+
key = nil
|
41
|
+
if options[:for]
|
42
|
+
query = options[:for]
|
43
|
+
if (where_values = query.where).length == 1
|
44
|
+
where_value = query.where.first
|
45
|
+
key = "#{query.table_name}:#{where_value[0]}#{where_value[1][0]}#{where_value[1][1]}"
|
46
|
+
else
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
elsif options[:at]
|
50
|
+
key = options[:at]
|
51
|
+
end
|
52
|
+
|
53
|
+
return nil if key.nil?
|
54
|
+
|
55
|
+
deflated = deflate_with_score(objects)
|
56
|
+
deflated.each do |object_and_score|
|
57
|
+
@cache.zadd key, *object_and_score
|
58
|
+
end
|
59
|
+
|
60
|
+
objects
|
61
|
+
end
|
62
|
+
|
63
|
+
def update(object, options={})
|
64
|
+
old_keys = object.to_keys(:old_values => true).select { |key| @cache.exists(key) }
|
65
|
+
new_keys = object.to_keys(:old_values => false).select { |key| @cache.exists(key) }
|
66
|
+
|
67
|
+
@cache.multi do
|
68
|
+
old_keys.each do |key|
|
69
|
+
destroy(object, :at => key, :old_values => true)
|
70
|
+
end
|
71
|
+
|
72
|
+
new_keys.each do |key|
|
73
|
+
insert([object], :at => key)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def destroy(object, options={})
|
79
|
+
@cache.zrem(options[:at], deflate(object, :old_values => options[:old_values]))
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear!
|
83
|
+
@cache.flushdb
|
84
|
+
end
|
85
|
+
|
86
|
+
def inflate(objects, options={})
|
87
|
+
return nil if objects.nil?
|
88
|
+
|
89
|
+
unless options[:for]
|
90
|
+
return objects
|
91
|
+
else
|
92
|
+
objects.map do |object|
|
93
|
+
obj = options[:for].klass.new
|
94
|
+
attributes = ActiveSupport::JSON.decode(object)
|
95
|
+
attributes.each do |attr, value|
|
96
|
+
obj.send(:"#{attr}=", value)
|
97
|
+
end
|
98
|
+
obj.changed_attributes.clear
|
99
|
+
obj.send(:instance_variable_set, "@new_record", false)
|
100
|
+
obj
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def deflate(object, options={})
|
106
|
+
attributes = if options[:old_values]
|
107
|
+
object.attributes.keys.inject({}) do |attributes, key|
|
108
|
+
attributes[key] = object.send(:"#{key}_was")
|
109
|
+
attributes
|
110
|
+
end
|
111
|
+
else
|
112
|
+
object.attributes
|
113
|
+
end
|
114
|
+
|
115
|
+
ActiveSupport::JSON.encode(attributes)
|
116
|
+
end
|
117
|
+
|
118
|
+
def deflate_with_score(objects, options={})
|
119
|
+
return nil if objects.nil?
|
120
|
+
|
121
|
+
score_method = options[:sorted_by] || :id
|
122
|
+
|
123
|
+
objects.map { |object| [object.send(score_method).to_i, deflate(object)] }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module CaChing
|
2
|
+
module Configuration
|
3
|
+
def configure
|
4
|
+
yield self
|
5
|
+
end
|
6
|
+
|
7
|
+
def cache=(cache)
|
8
|
+
@cache = cache
|
9
|
+
end
|
10
|
+
|
11
|
+
def cache
|
12
|
+
@cache
|
13
|
+
end
|
14
|
+
|
15
|
+
def disabled?
|
16
|
+
@disabled
|
17
|
+
end
|
18
|
+
|
19
|
+
def disabled=(d)
|
20
|
+
@disabled = d
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module CaChing
|
2
|
+
module Index
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
VALID_OPTIONS = [:order, :ttl, :limit]
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def index(field_name, options={})
|
9
|
+
raise CaChing::InvalidOptionError unless options.keys.inject(true) { |flag, key| flag && CaChing::Index::VALID_OPTIONS.include?(key) }
|
10
|
+
indexed_fields[field_name] = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def indexes?(field_name)
|
14
|
+
indexed_fields.has_key?(field_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def indexed_fields
|
18
|
+
@_indexed_fields ||= {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def indexed_fields
|
24
|
+
self.class.indexed_fields
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module CaChing
|
2
|
+
module Query
|
3
|
+
class Abstract
|
4
|
+
# Unceremoniously taken from cache-money
|
5
|
+
AND = /\s+AND\s+/i
|
6
|
+
OR = /\s+OR\s+/i
|
7
|
+
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
8
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
|
9
|
+
KEY_CMP_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+(=|<|<=|>|>=)\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
|
10
|
+
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
|
11
|
+
|
12
|
+
attr_accessor :klass
|
13
|
+
|
14
|
+
def initialize(active_record_collection, options={})
|
15
|
+
self.tap do
|
16
|
+
@collection = active_record_collection
|
17
|
+
@sql = active_record_collection.to_sql
|
18
|
+
@klass = active_record_collection.klass
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def table_name
|
23
|
+
@collection.table_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def where
|
27
|
+
raise UncacheableConditionError unless cacheable?
|
28
|
+
|
29
|
+
@collection.where_values.inject({}) do |hash, value|
|
30
|
+
if value.respond_to? :left
|
31
|
+
left = value.left.name.to_sym
|
32
|
+
right = value.right
|
33
|
+
if right == '?'
|
34
|
+
right = @collection.bind_values.find { |value| value.first.name == left.to_s }[1]
|
35
|
+
end
|
36
|
+
|
37
|
+
hash[left] = ['=', right]
|
38
|
+
else
|
39
|
+
value.split(AND).each do |value|
|
40
|
+
match = KEY_CMP_VALUE.match(value).captures
|
41
|
+
left, comparator, right = match[1].to_sym, match[2], match[3]
|
42
|
+
|
43
|
+
hash[left] = [comparator, right]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@where = hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def order
|
52
|
+
order_values = @collection.order_values.map { |v| v.split(',').map { |v| v.strip } }.flatten
|
53
|
+
|
54
|
+
order_values.inject({}) do |hash, value|
|
55
|
+
match = ORDER.match(value).captures
|
56
|
+
hash[match[1].to_sym] = match[2] == "ASC" ? :asc : :desc
|
57
|
+
|
58
|
+
hash
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def limit
|
63
|
+
@collection.limit_value
|
64
|
+
end
|
65
|
+
|
66
|
+
def offset
|
67
|
+
@collection.offset_value
|
68
|
+
end
|
69
|
+
|
70
|
+
def calculation?
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
# Formats the query to a key for the value store. Takes the form
|
75
|
+
# table_name:encode(field1, [operator1, value1])&encode(field2, [operator2, value2])...
|
76
|
+
def to_key
|
77
|
+
"#{table_name}:#{where.map { |field, operator_and_value| encode(field, operator_and_value) } * "&" }"
|
78
|
+
end
|
79
|
+
|
80
|
+
def primary_key?
|
81
|
+
@where ||= where
|
82
|
+
@where.keys.length == 1 && @where.keys.include?(@collection.primary_key.to_sym)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
# Encodes string to the form "field_name=value" where '=' can be any arbitrary
|
87
|
+
# operator.
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# encode('name', ['=', 'Andrew']) # => "name='Andrew'"
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# encode('age', ['>=', 21]) # => "age>=21"
|
94
|
+
def encode(field, operator_and_value)
|
95
|
+
operator, value = operator_and_value
|
96
|
+
"#{field}#{operator}\"#{value.to_s.gsub('"', '\"')}\""
|
97
|
+
end
|
98
|
+
|
99
|
+
def cacheable?
|
100
|
+
!(@collection.to_sql =~ OR)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class UncacheableConditionError < StandardError; end
|
105
|
+
end
|
106
|
+
end
|