factorylabs-cache-money 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README +205 -0
- data/TODO +17 -0
- data/UNSUPPORTED_FEATURES +14 -0
- data/config/environment.rb +6 -0
- data/config/memcache.yml +6 -0
- data/db/schema.rb +12 -0
- data/lib/cache_money.rb +54 -0
- data/lib/cash/accessor.rb +88 -0
- data/lib/cash/buffered.rb +126 -0
- data/lib/cash/config.rb +71 -0
- data/lib/cash/finders.rb +38 -0
- data/lib/cash/index.rb +207 -0
- data/lib/cash/local.rb +59 -0
- data/lib/cash/lock.rb +52 -0
- data/lib/cash/mock.rb +86 -0
- data/lib/cash/query/abstract.rb +162 -0
- data/lib/cash/query/calculation.rb +45 -0
- data/lib/cash/query/primary_key.rb +51 -0
- data/lib/cash/query/select.rb +16 -0
- data/lib/cash/request.rb +3 -0
- data/lib/cash/transactional.rb +42 -0
- data/lib/cash/util/array.rb +9 -0
- data/lib/cash/write_through.rb +72 -0
- data/spec/cash/accessor_spec.rb +167 -0
- data/spec/cash/active_record_spec.rb +221 -0
- data/spec/cash/calculations_spec.rb +67 -0
- data/spec/cash/finders_spec.rb +355 -0
- data/spec/cash/lock_spec.rb +87 -0
- data/spec/cash/order_spec.rb +166 -0
- data/spec/cash/transactional_spec.rb +574 -0
- data/spec/cash/window_spec.rb +195 -0
- data/spec/cash/write_through_spec.rb +223 -0
- data/spec/spec_helper.rb +56 -0
- metadata +113 -0
data/lib/cash/index.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
module Cash
|
2
|
+
class Index
|
3
|
+
attr_reader :attributes, :options
|
4
|
+
delegate :each, :hash, :to => :@attributes
|
5
|
+
delegate :get, :set, :expire, :find_every_without_cache, :calculate_without_cache, :calculate_with_cache, :incr, :decr, :primary_key, :to => :@active_record
|
6
|
+
|
7
|
+
DEFAULT_OPTIONS = { :ttl => 1.day }
|
8
|
+
|
9
|
+
def initialize(config, active_record, attributes, options = {})
|
10
|
+
@config, @active_record, @attributes, @options = config, active_record, Array(attributes).collect(&:to_s).sort, DEFAULT_OPTIONS.merge(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
case other
|
15
|
+
when Index
|
16
|
+
attributes == other.attributes
|
17
|
+
else
|
18
|
+
attributes == Array(other)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
alias_method :eql?, :==
|
22
|
+
|
23
|
+
module Commands
|
24
|
+
def add(object)
|
25
|
+
clone = object.shallow_clone
|
26
|
+
_, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
|
27
|
+
add_to_index_with_minimal_network_operations(new_attribute_value_pairs, clone)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(object)
|
31
|
+
clone = object.shallow_clone
|
32
|
+
old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
|
33
|
+
update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, clone)
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove(object)
|
37
|
+
old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
|
38
|
+
remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(object)
|
42
|
+
old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
|
43
|
+
key = cache_key(old_attribute_value_pairs)
|
44
|
+
expire(key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
include Commands
|
48
|
+
|
49
|
+
module Attributes
|
50
|
+
def ttl
|
51
|
+
@ttl ||= options[:ttl] || config.ttl
|
52
|
+
end
|
53
|
+
|
54
|
+
def order
|
55
|
+
@order ||= options[:order] || :asc
|
56
|
+
end
|
57
|
+
|
58
|
+
def limit
|
59
|
+
options[:limit]
|
60
|
+
end
|
61
|
+
|
62
|
+
def buffer
|
63
|
+
options[:buffer]
|
64
|
+
end
|
65
|
+
|
66
|
+
def window
|
67
|
+
limit && limit + buffer
|
68
|
+
end
|
69
|
+
end
|
70
|
+
include Attributes
|
71
|
+
|
72
|
+
def serialize_object(object)
|
73
|
+
primary_key? ? object : object.id
|
74
|
+
end
|
75
|
+
|
76
|
+
def matches?(query)
|
77
|
+
query.calculation? ||
|
78
|
+
(query.order == ['id', order] &&
|
79
|
+
(!limit || (query.limit && query.limit + query.offset <= limit)))
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def old_and_new_attribute_value_pairs(object)
|
84
|
+
old_attribute_value_pairs = []
|
85
|
+
new_attribute_value_pairs = []
|
86
|
+
@attributes.each do |name|
|
87
|
+
new_value = object.attributes[name]
|
88
|
+
original_value = object.send("#{name}_was")
|
89
|
+
old_attribute_value_pairs << [name, original_value]
|
90
|
+
new_attribute_value_pairs << [name, new_value]
|
91
|
+
end
|
92
|
+
[old_attribute_value_pairs, new_attribute_value_pairs]
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
|
96
|
+
if primary_key?
|
97
|
+
add_object_to_primary_key_cache(attribute_value_pairs, object)
|
98
|
+
else
|
99
|
+
add_object_to_cache(attribute_value_pairs, object)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def primary_key?
|
104
|
+
@attributes.size == 1 && @attributes.first == primary_key
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_object_to_primary_key_cache(attribute_value_pairs, object)
|
108
|
+
set(cache_key(attribute_value_pairs), [object], :ttl => ttl)
|
109
|
+
end
|
110
|
+
|
111
|
+
def cache_key(attribute_value_pairs)
|
112
|
+
attribute_value_pairs.flatten.join('/')
|
113
|
+
end
|
114
|
+
|
115
|
+
def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
|
116
|
+
return if invalid_cache_key?(attribute_value_pairs)
|
117
|
+
|
118
|
+
key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
|
119
|
+
if !cache_hit || overwrite
|
120
|
+
object_to_add = serialize_object(object)
|
121
|
+
objects = (cache_value + [object_to_add]).sort do |a, b|
|
122
|
+
(a <=> b) * (order == :asc ? 1 : -1)
|
123
|
+
end.uniq
|
124
|
+
objects = truncate_if_necessary(objects)
|
125
|
+
set(key, objects, :ttl => ttl)
|
126
|
+
incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def invalid_cache_key?(attribute_value_pairs)
|
131
|
+
attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_key_and_value_at_index(attribute_value_pairs)
|
135
|
+
key = cache_key(attribute_value_pairs)
|
136
|
+
cache_hit = true
|
137
|
+
cache_value = get(key) do
|
138
|
+
cache_hit = false
|
139
|
+
conditions = attribute_value_pairs.to_hash
|
140
|
+
find_every_without_cache(:select => primary_key, :conditions => conditions, :limit => window).collect do |object|
|
141
|
+
serialize_object(object)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
[key, cache_value, cache_hit]
|
145
|
+
end
|
146
|
+
|
147
|
+
def truncate_if_necessary(objects)
|
148
|
+
objects.slice(0, window || objects.size)
|
149
|
+
end
|
150
|
+
|
151
|
+
def calculate_at_index(operation, attribute_value_pairs)
|
152
|
+
conditions = attribute_value_pairs.to_hash
|
153
|
+
calculate_without_cache(operation, :all, :conditions => conditions)
|
154
|
+
end
|
155
|
+
|
156
|
+
def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
|
157
|
+
if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
|
158
|
+
remove_object_from_cache(old_attribute_value_pairs, object)
|
159
|
+
add_object_to_cache(new_attribute_value_pairs, object)
|
160
|
+
elsif primary_key?
|
161
|
+
add_object_to_primary_key_cache(new_attribute_value_pairs, object)
|
162
|
+
else
|
163
|
+
add_object_to_cache(new_attribute_value_pairs, object, false)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
|
168
|
+
old_attribute_value_pairs != new_attribute_value_pairs
|
169
|
+
end
|
170
|
+
|
171
|
+
def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
|
172
|
+
if primary_key?
|
173
|
+
remove_object_from_primary_key_cache(attribute_value_pairs, object)
|
174
|
+
else
|
175
|
+
remove_object_from_cache(attribute_value_pairs, object)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def remove_object_from_primary_key_cache(attribute_value_pairs, object)
|
180
|
+
set(cache_key(attribute_value_pairs), [], :ttl => ttl)
|
181
|
+
end
|
182
|
+
|
183
|
+
def remove_object_from_cache(attribute_value_pairs, object)
|
184
|
+
return if invalid_cache_key?(attribute_value_pairs)
|
185
|
+
|
186
|
+
key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
|
187
|
+
object_to_remove = serialize_object(object)
|
188
|
+
objects = cache_value - [object_to_remove]
|
189
|
+
objects = resize_if_necessary(attribute_value_pairs, objects)
|
190
|
+
set(key, objects, :ttl => ttl)
|
191
|
+
end
|
192
|
+
|
193
|
+
def resize_if_necessary(attribute_value_pairs, objects)
|
194
|
+
conditions = attribute_value_pairs.to_hash
|
195
|
+
key = cache_key(attribute_value_pairs)
|
196
|
+
count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
|
197
|
+
|
198
|
+
if limit && objects.size < limit && objects.size < count
|
199
|
+
find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
|
200
|
+
serialize_object(object)
|
201
|
+
end
|
202
|
+
else
|
203
|
+
objects
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/cash/local.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Cash
|
2
|
+
class Local
|
3
|
+
delegate :respond_to?, :to => :@remote_cache
|
4
|
+
|
5
|
+
def initialize(remote_cache)
|
6
|
+
@remote_cache = remote_cache
|
7
|
+
end
|
8
|
+
|
9
|
+
def cache_locally
|
10
|
+
@remote_cache = LocalBuffer.new(original_cache = @remote_cache)
|
11
|
+
yield
|
12
|
+
ensure
|
13
|
+
@remote_cache = original_cache
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method, *args, &block)
|
17
|
+
@remote_cache.send(method, *args, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class LocalBuffer
|
22
|
+
delegate :respond_to?, :to => :@remote_cache
|
23
|
+
|
24
|
+
def initialize(remote_cache)
|
25
|
+
@local_cache = {}
|
26
|
+
@remote_cache = remote_cache
|
27
|
+
end
|
28
|
+
|
29
|
+
def get(key, *options)
|
30
|
+
if @local_cache.has_key?(key)
|
31
|
+
@local_cache[key]
|
32
|
+
else
|
33
|
+
@local_cache[key] = @remote_cache.get(key, *options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set(key, value, *options)
|
38
|
+
@remote_cache.set(key, value, *options)
|
39
|
+
@local_cache[key] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(key, value, *options)
|
43
|
+
result = @remote_cache.add(key, value, *options)
|
44
|
+
if result == "STORED\r\n"
|
45
|
+
@local_cache[key] = value
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(key, *options)
|
51
|
+
@remote_cache.delete(key, *options)
|
52
|
+
@local_cache.delete(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(method, *args, &block)
|
56
|
+
@remote_cache.send(method, *args, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/cash/lock.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Cash
|
2
|
+
class Lock
|
3
|
+
class Error < RuntimeError; end
|
4
|
+
|
5
|
+
DEFAULT_RETRY = 5
|
6
|
+
DEFAULT_EXPIRY = 30
|
7
|
+
|
8
|
+
def initialize(cache)
|
9
|
+
@cache = cache
|
10
|
+
end
|
11
|
+
|
12
|
+
def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
|
13
|
+
if recursive_lock?(key)
|
14
|
+
yield
|
15
|
+
else
|
16
|
+
acquire_lock(key, lock_expiry, retries)
|
17
|
+
begin
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
release_lock(key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
|
26
|
+
retries.times do |count|
|
27
|
+
begin
|
28
|
+
response = @cache.add("lock/#{key}", Process.pid, lock_expiry)
|
29
|
+
return if response == "STORED\r\n"
|
30
|
+
raise Error if count == retries - 1
|
31
|
+
end
|
32
|
+
exponential_sleep(count) unless count == retries - 1
|
33
|
+
end
|
34
|
+
raise Error, "Couldn't acquire memcache lock for: #{key}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def release_lock(key)
|
38
|
+
@cache.delete("lock/#{key}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def exponential_sleep(count)
|
42
|
+
@runtime += Benchmark::measure { sleep((2**count) / 2.0) }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def recursive_lock?(key)
|
48
|
+
@cache.get("lock/#{key}") == Process.pid
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/cash/mock.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Cash
|
2
|
+
class Mock < HashWithIndifferentAccess
|
3
|
+
attr_accessor :servers
|
4
|
+
|
5
|
+
def get_multi(keys)
|
6
|
+
slice(*keys).collect { |k,v| [k, Marshal.load(v)] }.to_hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(key, value, ttl = 0, raw = false)
|
10
|
+
self[key] = marshal(value, raw)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(key, raw = false)
|
14
|
+
if raw
|
15
|
+
self[key]
|
16
|
+
else
|
17
|
+
if self.has_key?(key)
|
18
|
+
Marshal.load(self[key])
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def incr(key, amount = 1)
|
26
|
+
if self.has_key?(key)
|
27
|
+
self[key] = (self[key].to_i + amount).to_s
|
28
|
+
self[key].to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def decr(key, amount = 1)
|
33
|
+
if self.has_key?(key)
|
34
|
+
self[key] = (self[key].to_i - amount).to_s
|
35
|
+
self[key].to_i
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add(key, value, ttl = 0, raw = false)
|
40
|
+
if self.has_key?(key)
|
41
|
+
"NOT_STORED\r\n"
|
42
|
+
else
|
43
|
+
self[key] = marshal(value, raw)
|
44
|
+
"STORED\r\n"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def append(key, value)
|
49
|
+
set(key, get(key, true).to_s + value.to_s, nil, true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def namespace
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def flush_all
|
57
|
+
clear
|
58
|
+
end
|
59
|
+
|
60
|
+
def stats
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
|
64
|
+
def reset_runtime
|
65
|
+
[0, Hash.new(0)]
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def marshal(value, raw)
|
71
|
+
if raw
|
72
|
+
value.to_s
|
73
|
+
else
|
74
|
+
Marshal.dump(value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def unmarshal(marshaled_obj)
|
79
|
+
Marshal.load(marshaled_obj)
|
80
|
+
end
|
81
|
+
|
82
|
+
def deep_clone(obj)
|
83
|
+
unmarshal(marshal(obj))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Abstract
|
4
|
+
delegate :with_exclusive_scope, :get, :table_name, :indices, :find_from_ids_without_cache, :cache_key, :columns_hash, :to => :@active_record
|
5
|
+
|
6
|
+
def self.perform(*args)
|
7
|
+
new(*args).perform
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(active_record, options1, options2)
|
11
|
+
@active_record, @options1, @options2 = active_record, options1, options2 || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(find_options = {}, get_options = {})
|
15
|
+
if cache_config = cacheable?(@options1, @options2, find_options)
|
16
|
+
cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
|
17
|
+
|
18
|
+
misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
|
19
|
+
format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
|
20
|
+
else
|
21
|
+
uncacheable
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
DESC = /DESC/i
|
26
|
+
|
27
|
+
def order
|
28
|
+
@order ||= begin
|
29
|
+
if order_sql = @options1[:order] || @options2[:order]
|
30
|
+
matched, table_name, column_name, direction = *(ORDER.match(order_sql))
|
31
|
+
[column_name, direction =~ DESC ? :desc : :asc]
|
32
|
+
else
|
33
|
+
['id', :asc]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def limit
|
39
|
+
@limit ||= @options1[:limit] || @options2[:limit]
|
40
|
+
end
|
41
|
+
|
42
|
+
def offset
|
43
|
+
@offset ||= @options1[:offset] || @options2[:offset] || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def calculation?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def cacheable?(*optionss)
|
52
|
+
optionss.each { |options| return unless safe_options_for_cache?(options) }
|
53
|
+
partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
|
54
|
+
return if partial_indices.include?(nil)
|
55
|
+
attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
|
56
|
+
if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
|
57
|
+
if index.matches?(self)
|
58
|
+
[attribute_value_pairs, index]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def hit_or_miss(cache_keys, index, options)
|
64
|
+
misses, missed_keys = nil, nil
|
65
|
+
objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
|
66
|
+
misses = miss(missed_keys, @options1.merge(:limit => index.window))
|
67
|
+
serialize_objects(index, misses)
|
68
|
+
end
|
69
|
+
[misses, missed_keys, objects]
|
70
|
+
end
|
71
|
+
|
72
|
+
def cache_keys(attribute_value_pairs)
|
73
|
+
attribute_value_pairs.flatten.join('/')
|
74
|
+
end
|
75
|
+
|
76
|
+
def safe_options_for_cache?(options)
|
77
|
+
return false unless options.kind_of?(Hash)
|
78
|
+
options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
|
79
|
+
end
|
80
|
+
|
81
|
+
def attribute_value_pairs_for_conditions(conditions)
|
82
|
+
case conditions
|
83
|
+
when Hash
|
84
|
+
conditions.to_a.collect { |key, value| [key.to_s, value] }
|
85
|
+
when String
|
86
|
+
parse_indices_from_condition(conditions)
|
87
|
+
when Array
|
88
|
+
parse_indices_from_condition(*conditions)
|
89
|
+
when NilClass
|
90
|
+
[]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
AND = /\s+AND\s+/i
|
95
|
+
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
96
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
|
97
|
+
KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
|
98
|
+
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
|
99
|
+
|
100
|
+
def parse_indices_from_condition(conditions = '', *values)
|
101
|
+
values = values.dup
|
102
|
+
conditions.split(AND).inject([]) do |indices, condition|
|
103
|
+
matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
|
104
|
+
if matched
|
105
|
+
value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
|
106
|
+
indices << [column_name, value]
|
107
|
+
else
|
108
|
+
return nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def indexed_on?(attributes)
|
114
|
+
indices.detect { |index| index == attributes }
|
115
|
+
end
|
116
|
+
alias_method :index_for, :indexed_on?
|
117
|
+
|
118
|
+
def format_results(cache_keys, objects)
|
119
|
+
return objects if objects.blank?
|
120
|
+
|
121
|
+
objects = convert_to_array(cache_keys, objects)
|
122
|
+
objects = apply_limits_and_offsets(objects, @options1)
|
123
|
+
deserialize_objects(objects)
|
124
|
+
end
|
125
|
+
|
126
|
+
def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
|
127
|
+
missed_keys == cache_keys ? misses : objects
|
128
|
+
end
|
129
|
+
|
130
|
+
def serialize_objects(index, objects)
|
131
|
+
Array(objects).collect { |missed| index.serialize_object(missed) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def convert_to_array(cache_keys, object)
|
135
|
+
if object.kind_of?(Hash)
|
136
|
+
cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
|
137
|
+
else
|
138
|
+
Array(object)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def apply_limits_and_offsets(results, options)
|
143
|
+
results.slice((options[:offset] || 0), (options[:limit] || results.length))
|
144
|
+
end
|
145
|
+
|
146
|
+
def deserialize_objects(objects)
|
147
|
+
if objects.first.kind_of?(ActiveRecord::Base)
|
148
|
+
objects
|
149
|
+
else
|
150
|
+
cache_keys = objects.collect { |id| "id/#{id}" }
|
151
|
+
objects = get(cache_keys, &method(:find_from_keys))
|
152
|
+
convert_to_array(cache_keys, objects)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_from_keys(*missing_keys)
|
157
|
+
missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
|
158
|
+
find_from_ids_without_cache(missing_ids, {})
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Calculation < Abstract
|
4
|
+
delegate :calculate_without_cache, :incr, :to => :@active_record
|
5
|
+
|
6
|
+
def initialize(active_record, operation, column, options1, options2)
|
7
|
+
super(active_record, options1, options2)
|
8
|
+
@operation, @column = operation, column
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform
|
12
|
+
super({}, :raw => true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def calculation?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def miss(_, __)
|
21
|
+
calculate_without_cache(@operation, @column, @options1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def uncacheable
|
25
|
+
calculate_without_cache(@operation, @column, @options1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_results(_, objects)
|
29
|
+
objects.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize_objects(_, objects)
|
33
|
+
objects.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def cacheable?(*optionss)
|
37
|
+
@column == :all && super(*optionss)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_keys(attribute_value_pairs)
|
41
|
+
"#{super(attribute_value_pairs)}/#{@operation}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class PrimaryKey < Abstract
|
4
|
+
def initialize(active_record, ids, options1, options2)
|
5
|
+
super(active_record, options1, options2)
|
6
|
+
@expects_array = ids.first.kind_of?(Array)
|
7
|
+
@original_ids = ids
|
8
|
+
@ids = ids.flatten.compact.uniq.collect do |object|
|
9
|
+
object.respond_to?(:quoted_id) ? object.quoted_id : object.to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
return [] if @expects_array && @ids.empty?
|
15
|
+
raise ActiveRecord::RecordNotFound if @ids.empty?
|
16
|
+
|
17
|
+
super(:conditions => { :id => @ids.first })
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def deserialize_objects(objects)
|
22
|
+
convert_to_active_record_collection(super(objects))
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_keys(attribute_value_pairs)
|
26
|
+
@ids.collect { |id| "id/#{id}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def miss(missing_keys, options)
|
31
|
+
find_from_keys(*missing_keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
def uncacheable
|
35
|
+
find_from_ids_without_cache(@original_ids, @options1)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def convert_to_active_record_collection(objects)
|
40
|
+
case objects.size
|
41
|
+
when 0
|
42
|
+
raise ActiveRecord::RecordNotFound
|
43
|
+
when 1
|
44
|
+
@expects_array ? objects : objects.first
|
45
|
+
else
|
46
|
+
objects
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Select < Abstract
|
4
|
+
delegate :find_every_without_cache, :to => :@active_record
|
5
|
+
|
6
|
+
protected
|
7
|
+
def miss(_, miss_options)
|
8
|
+
find_every_without_cache(miss_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def uncacheable
|
12
|
+
find_every_without_cache(@options1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cash/request.rb
ADDED