Skip to content

Commit 8f93b46

Browse files
authored
Merge pull request #1784 from codidact/0valt/1783/collection-caching
Ensure values are normalized when reading collections from cache
2 parents 4682a37 + 520ed22 commit 8f93b46

File tree

2 files changed

+55
-11
lines changed

2 files changed

+55
-11
lines changed

lib/namespaced_env_cache.rb

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,34 +72,34 @@ def persistent(name, **opts, &block)
7272
end
7373
end
7474

75-
##
7675
# Cache an ActiveRecord collection. Supports only a basic collection of one type of object. Column selections or
7776
# joins etc. will NOT be respected when the collection is read back out.
7877
# @param name [String] cache key name
7978
# @param value [ActiveRecord::Relation] collection to cache
8079
# @param opts [Hash] options hash - any unlisted options will be passed to the underlying cache
8180
# @option opts [Boolean] :include_community whether to include the community ID in the cache key
8281
def write_collection(name, value, **opts)
83-
types = value.map(&:class).uniq
84-
if types.size > 1
85-
raise TypeError, "Can't cache more than one type of object via write_collection"
86-
end
87-
88-
data = [types[0].to_s, *value.map(&:id)]
89-
namespaced = construct_ns_key(name, include_community: include_community(opts))
90-
@underlying.write(namespaced, data, **opts)
82+
check_collection(value)
83+
data = NamespacedEnvCache.normalize_collection(value)
84+
write_collection_data(name, data, **opts)
9185
end
9286

93-
##
9487
# Read an ActiveRecord collection from cache. Returns a basic collection of the records that were cached, with
9588
# no selects or joins applied.
9689
# @param name [String] cache key name
9790
# @param opts [Hash] options hash - any unlisted options will be passed to the underlying cache
9891
# @options opts [Boolean] :include_community whether to include the community ID in the cache key
92+
# @return [ActiveRecord::Relation, nil]
9993
def read_collection(name, **opts)
10094
namespaced = construct_ns_key(name, include_community: include_community(opts))
10195
data = @underlying.read(namespaced, **opts)
10296
return nil if data.nil?
97+
98+
if data.is_a?(ActiveRecord::Relation)
99+
data = NamespacedEnvCache.normalize_collection(data)
100+
write_collection_data(name, data, **opts)
101+
end
102+
103103
type = data.slice!(0)
104104
begin
105105
type.constantize.where(id: data)
@@ -109,7 +109,6 @@ def read_collection(name, **opts)
109109
end
110110
end
111111

112-
##
113112
# Fetch an ActiveRecord collection from cache if it is present, otherwise cache the value returned by +block+.
114113
# @param name [String] cache key name
115114
# @param opts [Hash] options hash - any unlisted options will be passed to the underlying cache
@@ -131,6 +130,13 @@ def fetch_collection(name, **opts, &block)
131130
end
132131
end
133132

133+
# Normalizes a given ActiveRecord collection for use with the cache
134+
# @param value [ActiveRecord::Relation] collection to normalize
135+
# @return [[String, Integer, Integer, ...]]
136+
def self.normalize_collection(value)
137+
[value[0].class.to_s, *value.map(&:id)]
138+
end
139+
134140
# We have to statically report that we support cache versioning even though this depends on the underlying class.
135141
# However, this is not really a problem since all cache stores provided by activesupport support the feature and
136142
# we only use the redis cache (by activesupport) for QPixel.
@@ -140,10 +146,28 @@ def self.supports_cache_versioning?
140146

141147
private
142148

149+
# Raises an error if a given collection is not cacheable
150+
# @param value [ActiveRecord::Relation] collection to check
151+
def check_collection(value)
152+
types = value.map(&:class).uniq
153+
if types.size > 1
154+
raise TypeError, "Can't cache more than one type of object"
155+
end
156+
end
157+
143158
def construct_ns_key(key, include_community: true)
144159
key = expanded_key(key)
145160
c_id = RequestContext.community_id if include_community
146161
"#{Rails.env}://#{[c_id, key].compact.join('/')}"
147162
end
163+
164+
# Writes normalized collection data to the underlying cache
165+
# @param name [String] cache key name
166+
# @param data [[String, Integer, Integer, ...]] normalized collection data
167+
# @param opts [Hash] options hash - see #write_collection
168+
def write_collection_data(name, data, **opts)
169+
namespaced = construct_ns_key(name, include_community: include_community(opts))
170+
@underlying.write(namespaced, data, **opts)
171+
end
148172
end
149173
end

test/lib/namespaced_env_cache_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ class NamespacedEnvCacheTest < ActiveSupport::TestCase
1717
end
1818
end
1919

20+
test 'read_collection should not break on cached ActiveRecord::Relation objects' do
21+
@cache.write('test_posts_cache', Post.where(user: users(:standard_user)))
22+
23+
data = assert_nothing_raised do
24+
@cache.fetch_collection('test_posts_cache')
25+
end
26+
27+
assert_not_nil data
28+
end
29+
2030
test 'fetch_collection' do
2131
data = assert_nothing_raised do
2232
@cache.fetch_collection('test_posts_cache') do
@@ -30,9 +40,11 @@ class NamespacedEnvCacheTest < ActiveSupport::TestCase
3040
@cache.fetch_collection('test_posts_cache') do
3141
Post.where(user: users(:standard_user))
3242
end
43+
3344
data = assert_nothing_raised do
3445
@cache.fetch_collection('test_posts_cache')
3546
end
47+
3648
assert_not_nil data
3749
end
3850

@@ -48,6 +60,14 @@ class NamespacedEnvCacheTest < ActiveSupport::TestCase
4860
end
4961
end
5062

63+
test 'normalize_collection should correctly prepare collections for caching' do
64+
collection = Post.where(user: users(:standard_user))
65+
normalized = QPixel::NamespacedEnvCache.normalize_collection(collection)
66+
67+
assert_equal Post.name, normalized.shift
68+
assert(collection.to_a.all? { |post| normalized.include?(post.id) })
69+
end
70+
5171
private
5272

5373
def new_cache_store

0 commit comments

Comments
 (0)