support_table_cache 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +78 -12
- data/VERSION +1 -1
- data/lib/support_table_cache/associations.rb +51 -0
- data/lib/support_table_cache/find_by_override.rb +74 -0
- data/lib/support_table_cache/memory_cache.rb +53 -0
- data/lib/support_table_cache/relation_override.rb +88 -0
- data/lib/support_table_cache.rb +185 -45
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d66ca180334bd4b561b514dab26606f7ddc0979136a3cbca0479982734ad5703
|
4
|
+
data.tar.gz: 0e156f0ea38520098710748d1e5ecad71df3aecdb0df4ff78f8be92d075dbd3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f3b29d73e1cb683689888513b87ae6ceff74c871744ebdb30bb271789b6c4942355c773de4fad80a0d5f4d65f35462b3d49043f7119858a725ce39243e74e47
|
7
|
+
data.tar.gz: 9a2b3e9d9a79f3b91178f1218668d4b6d388397e85e7863e4e4746a9e89d934c3e012a6d709099671073dd2e03035842d22fcb9e15c9b8ad3cf2062cc56074d3
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## 1.1.0
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Added fetch_by and fetch_by! methods that can verify the result will be cacheable.
|
11
|
+
- Allow configuring cache storage on a per class basis.
|
12
|
+
- Allow disabling caching on per class basis.
|
13
|
+
- Added optimized in-memory cache implementation.
|
14
|
+
- Added support for caching belongs to assocations.
|
15
|
+
- Added test mode to intialize new caches within a test block.
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
- Changed fiber local variables used for disabling the cache to thread local variables.
|
19
|
+
- Using find_by! on a relation will now use the cache.
|
20
|
+
|
21
|
+
## 1.0.1
|
22
|
+
|
23
|
+
### Added
|
24
|
+
- Preserve scope on relations terminated with a `find_by`.
|
25
|
+
|
7
26
|
## 1.0.0
|
8
27
|
|
9
28
|
### Added
|
data/README.md
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
[![Continuous Integration](https://github.com/bdurand/support_table_cache/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/support_table_cache/actions/workflows/continuous_integration.yml)
|
4
4
|
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
5
5
|
|
6
|
-
This gem adds caching for ActiveRecord support table models. These
|
6
|
+
This gem adds caching for ActiveRecord support table models. These models have a unique key (i.e. a unique `name` attribute, etc.) and a limited number of entries (a few hundred at most). These are often models added to normalize the data structure and are also known as lookup tables.
|
7
7
|
|
8
|
-
Rows from these kinds of tables are rarely inserted, updated, or deleted, but are queried very frequently. To take advantage of this behavior, this gem adds automatic caching for records when using the `find_by` method
|
8
|
+
Rows from these kinds of tables are rarely inserted, updated, or deleted, but are queried very frequently. To take advantage of this behavior, this gem adds automatic caching for records when using the `find_by` method and `belongs_to` associations.
|
9
9
|
|
10
|
-
For instance, suppose you have a model `Status` that has a unique name attribute and you need to process a
|
10
|
+
For instance, suppose you have a model `Status` that has a unique name attribute and you need to process a lot of records from a data source that includes the status name. In order to do anything, you'll need to look up each status by name to get the database id:
|
11
11
|
|
12
12
|
```ruby
|
13
13
|
params.each do |data|
|
@@ -16,11 +16,11 @@ params.each do |data|
|
|
16
16
|
end
|
17
17
|
```
|
18
18
|
|
19
|
-
With this gem, you can avoid the database query
|
19
|
+
With this gem, you can avoid the database query associated with the `find_by` call. You don't need to alter your code in any way other than to include `SupportTableCache` in your model and telling it the attributes that comprise a unique key, which can be used for caching.
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
To use the gem, you need to include it in you models and then specify which attributes can be used for caching with the `cache_by` method. A caching attribute must be a unique key on the model. For a composite key, you can specify an array of attributes. If any of the attributes are case
|
23
|
+
To use the gem, you need to include it in you models and then specify which attributes can be used for caching with the `cache_by` method. A caching attribute must be a unique key on the model. For a composite unique key, you can specify an array of attributes. If any of the attributes are case-insensitive strings, you need to specify that as well.
|
24
24
|
|
25
25
|
```ruby
|
26
26
|
class MyModel < ApplicationRecord
|
@@ -36,14 +36,17 @@ To use the gem, you need to include it in you models and then specify which attr
|
|
36
36
|
# Uses cache on a composite key
|
37
37
|
MyModel.find_by(group: "first", name: "One")
|
38
38
|
|
39
|
-
#
|
39
|
+
# Uses cache on a composite key with scoping
|
40
|
+
MyModel.where(group: "first").find_by(name: "One")
|
41
|
+
|
42
|
+
# Does not use cache because the value is not defined as a cacheable key
|
40
43
|
MyModel.find_by(value: 1)
|
41
44
|
|
42
|
-
# Does not use caching
|
45
|
+
# Does not use caching because not using the find_by method
|
43
46
|
MyModel.where(id: 1).first
|
44
47
|
```
|
45
48
|
|
46
|
-
By default, records will be cleaned up from the cache only when they are modified. However, you can
|
49
|
+
By default, records will be cleaned up from the cache only when they are modified. However, you can change this by setting a time to live on the model after which records will be removed from the cache.
|
47
50
|
|
48
51
|
```ruby
|
49
52
|
class MyModel < ApplicationRecord
|
@@ -53,26 +56,89 @@ By default, records will be cleaned up from the cache only when they are modifie
|
|
53
56
|
end
|
54
57
|
```
|
55
58
|
|
56
|
-
|
59
|
+
### Setting the Cache
|
60
|
+
|
61
|
+
If you are in a Rails application, the `Rails.cache` will be used by default to cache records. Otherwise, you need to set the `ActiveSupport::Cache::CacheStore` instance to use.
|
57
62
|
|
58
63
|
```ruby
|
59
64
|
SupportTableCache.cache = ActiveSupport::Cache::MemoryStore.new
|
60
65
|
```
|
61
66
|
|
62
|
-
You can also
|
67
|
+
You can also set a cache per class. For instance, you can set an in-memory cache on models that are never changed to avoid a network round trip to the cache server. You can use the special value `:memory` to do this.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class MyModel < ApplicationRecord
|
71
|
+
include SupportTableCache
|
72
|
+
|
73
|
+
self.support_table_cache = :memory
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
Note that in-memory caches exist separately within each process and will not be cleared when records are changed in the database. The only way to refresh elements in an in-memory cache is to restart the process or set the `support_table_cache_ttl` value so that the entries will expire.
|
78
|
+
|
79
|
+
### Disabling Caching
|
80
|
+
|
81
|
+
You can disable the cache within a block either globally or only for a specific class. If the cache is disabled, then all queries will pass through to the database.
|
63
82
|
|
64
83
|
```ruby
|
65
84
|
# Disable the cache globally
|
66
85
|
SupportTableCache.disable
|
67
86
|
|
68
87
|
SupportTableCache.enable do
|
69
|
-
# Re-enable the cache
|
88
|
+
# Re-enable the cache within a block
|
70
89
|
SupportTableCache.disable do
|
71
90
|
# Disable it again
|
91
|
+
MySupportModel.enable_cache do
|
92
|
+
# Re-enable it only for the MySupportModel class
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
### Caching Belongs to Associations
|
99
|
+
|
100
|
+
You can also cache belongs to associations to cacheable models.
|
101
|
+
|
102
|
+
To do this, you include the `SupportTableCache::Associations` module in your model and then call `cache_belongs_to` to specify which associations should be cached. You must define the association first with `belongs_to` before you can call `cache_belongs_to`. You can include `SupportTableCache::Associations` in `ApplicationRecord` if you want this behavior available to all of your models.
|
103
|
+
|
104
|
+
The target class for the association must include the `SupportTableCache` module.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class ParentModel < ApplicationRecord
|
108
|
+
include SupportTableCache::Associations
|
109
|
+
|
110
|
+
belongs_to :my_model
|
111
|
+
cache_belongs_to :my_model
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
You can include `SupportTableCache::Associations` in your `ApplicationRecord` class to make association caching available on all models.
|
116
|
+
|
117
|
+
### Testing
|
118
|
+
|
119
|
+
Caching may interfere with tests by allowing data created in one test to leak into subsequent tests. You can resolve this by wrapping your tests with the `SupportTableCache.testing!` method.
|
120
|
+
|
121
|
+
```
|
122
|
+
# Rspec
|
123
|
+
RSpec.configure do |config|
|
124
|
+
config.around do |example|
|
125
|
+
SupportTableCache.testing! { example.run }
|
72
126
|
end
|
73
127
|
end
|
128
|
+
|
129
|
+
# MiniTest (with the minitest-around gem)
|
130
|
+
class MiniTest::Spec
|
131
|
+
around do |tests|
|
132
|
+
SupportTableCache.testing!(&tests)
|
133
|
+
end
|
134
|
+
=end
|
135
|
+
|
74
136
|
```
|
75
137
|
|
138
|
+
### Maintaining Data
|
139
|
+
|
140
|
+
You can use the companion [support_table_data gem](https://github.com/bdurand/support_table_data) to provide functionality for loading static data into your support tables as well as adding helper functions to make looking up specific rows much easier.
|
141
|
+
|
76
142
|
## Installation
|
77
143
|
|
78
144
|
Add this line to your application's Gemfile:
|
@@ -81,7 +147,7 @@ Add this line to your application's Gemfile:
|
|
81
147
|
gem "support_table_cache"
|
82
148
|
```
|
83
149
|
|
84
|
-
|
150
|
+
Then execute:
|
85
151
|
```bash
|
86
152
|
$ bundle
|
87
153
|
```
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.1.0
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SupportTableCache
|
4
|
+
# Extension to non-support table models for caching belongs_to associations to support tables.
|
5
|
+
module Associations
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Specify that a belongs_to association should use the cache. This will override the reader method
|
10
|
+
# for the association so that it queries from the cache. The association must already be defined.
|
11
|
+
#
|
12
|
+
# If you need cached associations to be cleared when data changes, then the associated class will
|
13
|
+
# need to include SupportTableCache and cache by the primary key for the association.
|
14
|
+
#
|
15
|
+
# @param association_name [Symbol, String] The association name to cache.
|
16
|
+
# @return [void]
|
17
|
+
# @raise ArgumentError if the association is not defined or if it has a runtime scope.
|
18
|
+
def cache_belongs_to(association_name)
|
19
|
+
reflection = reflections[association_name.to_s]
|
20
|
+
|
21
|
+
unless reflection&.belongs_to?
|
22
|
+
raise ArgumentError.new("The belongs_to #{association_name} association is not defined")
|
23
|
+
end
|
24
|
+
|
25
|
+
if reflection.scopes.present?
|
26
|
+
raise ArgumentError.new("Cannot cache belongs_to #{association_name} association because it has a scope")
|
27
|
+
end
|
28
|
+
|
29
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
30
|
+
def #{association_name}_with_cache
|
31
|
+
foreign_key = self.send(#{reflection.foreign_key.inspect})
|
32
|
+
return nil if foreign_key.nil?
|
33
|
+
key = [#{reflection.class_name.inspect}, {#{reflection.association_primary_key.inspect} => foreign_key}]
|
34
|
+
cache = #{reflection.class_name}.send(:current_support_table_cache)
|
35
|
+
ttl = #{reflection.class_name}.send(:support_table_cache_ttl)
|
36
|
+
if cache
|
37
|
+
cache.fetch(key, expires_in: ttl) do
|
38
|
+
#{association_name}_without_cache
|
39
|
+
end
|
40
|
+
else
|
41
|
+
#{association_name}_without_cache
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :#{association_name}_without_cache, :#{association_name}
|
46
|
+
alias_method :#{association_name}, :#{association_name}_with_cache
|
47
|
+
RUBY
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SupportTableCache
|
4
|
+
# @api private
|
5
|
+
module FindByOverride
|
6
|
+
# Override for the find_by method that looks in the cache first.
|
7
|
+
def find_by(*args)
|
8
|
+
cache = current_support_table_cache
|
9
|
+
return super if cache.nil?
|
10
|
+
|
11
|
+
cache_key = nil
|
12
|
+
attributes = (args.size == 1 && args.first.is_a?(Hash) ? args.first.stringify_keys : {})
|
13
|
+
|
14
|
+
if respond_to?(:scope_attributes) && scope_attributes.present?
|
15
|
+
attributes = scope_attributes.stringify_keys.merge(attributes)
|
16
|
+
end
|
17
|
+
|
18
|
+
if attributes.present?
|
19
|
+
support_table_cache_by_attributes.each do |attribute_names, case_sensitive, where|
|
20
|
+
where&.each do |name, value|
|
21
|
+
if attributes.include?(name) && attributes[name] == value
|
22
|
+
attributes.delete(name)
|
23
|
+
else
|
24
|
+
return super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
cache_key = SupportTableCache.cache_key(self, attributes, attribute_names, case_sensitive)
|
28
|
+
break if cache_key
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if cache_key
|
33
|
+
cache.fetch(cache_key, expires_in: support_table_cache_ttl) { super }
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Same as find_by, but performs a safety check to confirm the query will hit the cache.
|
40
|
+
#
|
41
|
+
# @param attributes [Hash] Attributes to find the record by.
|
42
|
+
# @raise ArgumentError if the query cannot use the cache.
|
43
|
+
def fetch_by(attributes)
|
44
|
+
find_by_attribute_names = support_table_find_by_attribute_names(attributes)
|
45
|
+
unless support_table_cache_by_attributes.any? { |attribute_names, _ci, _where| attribute_names == find_by_attribute_names }
|
46
|
+
raise ArgumentError.new("#{name} does not cache queries by #{find_by_attribute_names.to_sentence}")
|
47
|
+
end
|
48
|
+
find_by(attributes)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Same as find_by!, but performs a safety check to confirm the query will hit the cache.
|
52
|
+
#
|
53
|
+
# @param attributes [Hash] Attributes to find the record by.
|
54
|
+
# @raise ArgumentError if the query cannot use the cache.
|
55
|
+
# @raise ActiveRecord::RecordNotFound if no record is found.
|
56
|
+
def fetch_by!(attributes)
|
57
|
+
value = fetch_by(attributes)
|
58
|
+
if value.nil?
|
59
|
+
raise ActiveRecord::RecordNotFound.new("Couldn't find #{name}", name)
|
60
|
+
end
|
61
|
+
value
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def support_table_find_by_attribute_names(attributes)
|
67
|
+
attributes ||= {}
|
68
|
+
if respond_to?(:scope_attributes) && scope_attributes.present?
|
69
|
+
attributes = scope_attributes.merge(attributes)
|
70
|
+
end
|
71
|
+
attributes.keys.map(&:to_s).sort
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SupportTableCache
|
4
|
+
# An optimized cache implementation that can be used when all records can easily fit
|
5
|
+
# in memory and are never changed. It is intended for use with small, static support
|
6
|
+
# tables only.
|
7
|
+
#
|
8
|
+
# This cache will not store nil values. This is to prevent the cache from filling up with
|
9
|
+
# cache misses because there is no purging mechanism.
|
10
|
+
class MemoryCache
|
11
|
+
def initialize
|
12
|
+
@cache = {}
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch(key, expires_in: nil)
|
17
|
+
serialized_value, expire_at = @cache[key]
|
18
|
+
if serialized_value.nil? || (expire_at && expire_at < Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
19
|
+
value = yield if block_given?
|
20
|
+
return nil if value.nil?
|
21
|
+
write(key, value, expires_in: expires_in)
|
22
|
+
serialized_value = Marshal.dump(value)
|
23
|
+
end
|
24
|
+
Marshal.load(serialized_value)
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(key)
|
28
|
+
fetch(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(key, value, expires_in: nil)
|
32
|
+
return if value.nil?
|
33
|
+
|
34
|
+
if expires_in
|
35
|
+
expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in
|
36
|
+
end
|
37
|
+
|
38
|
+
serialized_value = Marshal.dump(value)
|
39
|
+
|
40
|
+
@mutex.synchronize do
|
41
|
+
@cache[key] = [serialized_value, expire_at]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(key)
|
46
|
+
@cache.delete(key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear
|
50
|
+
@cache.clear
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SupportTableCache
|
4
|
+
# @api private
|
5
|
+
|
6
|
+
module RelationOverride
|
7
|
+
# Override for the find_by method that looks in the cache first.
|
8
|
+
def find_by(*args)
|
9
|
+
return super unless klass.include?(SupportTableCache)
|
10
|
+
|
11
|
+
cache = klass.send(:current_support_table_cache)
|
12
|
+
return super if cache.nil?
|
13
|
+
|
14
|
+
cache_key = nil
|
15
|
+
attributes = (args.size == 1 && args.first.is_a?(Hash) ? args.first.stringify_keys : {})
|
16
|
+
|
17
|
+
# Apply any attributes from the current relation chain
|
18
|
+
if scope_attributes.present?
|
19
|
+
attributes = scope_attributes.stringify_keys.merge(attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
if attributes.present?
|
23
|
+
support_table_cache_by_attributes.each do |attribute_names, case_sensitive, where|
|
24
|
+
where&.each do |name, value|
|
25
|
+
if attributes.include?(name) && attributes[name] == value
|
26
|
+
attributes.delete(name)
|
27
|
+
else
|
28
|
+
return super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
cache_key = SupportTableCache.cache_key(klass, attributes, attribute_names, case_sensitive)
|
32
|
+
break if cache_key
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if cache_key
|
37
|
+
cache.fetch(cache_key, expires_in: support_table_cache_ttl) { super }
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Override for the find_by! method that looks in the cache first.
|
44
|
+
# @raise ActiveRecord::RecordNotFound if no record is found.
|
45
|
+
def find_by!(*args)
|
46
|
+
value = find_by(*args)
|
47
|
+
unless value
|
48
|
+
raise ActiveRecord::RecordNotFound.new("Couldn't find #{klass.name}", klass.name)
|
49
|
+
end
|
50
|
+
value
|
51
|
+
end
|
52
|
+
|
53
|
+
# Same as find_by, but performs a safety check to confirm the query will hit the cache.
|
54
|
+
#
|
55
|
+
# @param attributes [Hash] Attributes to find the record by.
|
56
|
+
# @raise ArgumentError if the query cannot use the cache.
|
57
|
+
def fetch_by(attributes)
|
58
|
+
find_by_attribute_names = support_table_find_by_attribute_names(attributes)
|
59
|
+
unless klass.support_table_cache_by_attributes.any? { |attribute_names, _ci| attribute_names == find_by_attribute_names }
|
60
|
+
raise ArgumentError.new("#{name} does not cache queries by #{find_by_attribute_names.to_sentence}")
|
61
|
+
end
|
62
|
+
find_by(attributes)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Same as find_by!, but performs a safety check to confirm the query will hit the cache.
|
66
|
+
#
|
67
|
+
# @param attributes [Hash] Attributes to find the record by.
|
68
|
+
# @raise ArgumentError if the query cannot use the cache.
|
69
|
+
# @raise ActiveRecord::RecordNotFound if no record is found.
|
70
|
+
def fetch_by!(attributes)
|
71
|
+
value = fetch_by(attributes)
|
72
|
+
if value.nil?
|
73
|
+
raise ActiveRecord::RecordNotFound.new("Couldn't find #{klass.name}", klass.name)
|
74
|
+
end
|
75
|
+
value
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def support_table_find_by_attribute_names(attributes)
|
81
|
+
attributes ||= {}
|
82
|
+
if scope_attributes.present?
|
83
|
+
attributes = scope_attributes.merge(attributes)
|
84
|
+
end
|
85
|
+
attributes.keys.map(&:to_s).sort
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/support_table_cache.rb
CHANGED
@@ -1,55 +1,166 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require_relative "support_table_cache/associations"
|
4
|
+
require_relative "support_table_cache/find_by_override"
|
5
|
+
require_relative "support_table_cache/relation_override"
|
6
|
+
require_relative "support_table_cache/memory_cache"
|
7
|
+
|
8
|
+
# This concern can be added to a model to add the ability to look up entries in the table
|
9
|
+
# using Rails.cache when calling find_by rather than hitting the database every time.
|
6
10
|
module SupportTableCache
|
7
11
|
extend ActiveSupport::Concern
|
8
12
|
|
9
13
|
included do
|
14
|
+
# @api private Used to store the list of attribute names used for caching.
|
10
15
|
class_attribute :support_table_cache_by_attributes, instance_accessor: false
|
16
|
+
|
17
|
+
# Set the time to live in seconds for records in the cache.
|
11
18
|
class_attribute :support_table_cache_ttl, instance_accessor: false
|
12
19
|
|
20
|
+
# @api private
|
21
|
+
class_attribute :support_table_cache_impl, instance_accessor: false
|
22
|
+
|
23
|
+
unless ActiveRecord::Relation.include?(RelationOverride)
|
24
|
+
ActiveRecord::Relation.prepend(RelationOverride)
|
25
|
+
end
|
26
|
+
|
13
27
|
class << self
|
14
28
|
prepend FindByOverride unless include?(FindByOverride)
|
15
29
|
private :support_table_cache_by_attributes=
|
30
|
+
private :support_table_cache_impl
|
31
|
+
private :support_table_cache_impl=
|
16
32
|
end
|
17
33
|
|
18
34
|
after_commit :support_table_clear_cache_entries
|
19
35
|
end
|
20
36
|
|
21
|
-
|
37
|
+
module ClassMethods
|
38
|
+
# Disable the caching behavior for this class within the block. The disabled setting
|
39
|
+
# for a class will always take precedence over the global setting.
|
40
|
+
#
|
41
|
+
# @param disabled [Boolean] Caching will be disabled if this is true and enabled if false.
|
42
|
+
# @yieldreturn The return value of the block.
|
43
|
+
def disable_cache(disabled = true, &block)
|
44
|
+
varname = "support_table_cache_disabled:#{name}"
|
45
|
+
save_val = Thread.current.thread_variable_get(varname)
|
46
|
+
begin
|
47
|
+
Thread.current.thread_variable_set(varname, !!disabled)
|
48
|
+
ensure
|
49
|
+
Thread.current.thread_variable_set(varname, save_val)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Enable the caching behavior for this class within the block. The enabled setting
|
54
|
+
# for a class will always take precedence over the global setting.
|
55
|
+
#
|
56
|
+
# @return [void]
|
57
|
+
def enable_cache
|
58
|
+
disable_cache(false, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Load all records into the cache. You should only call this method on small tables with
|
62
|
+
# a few dozen rows at most because it will load each row one at a time.
|
63
|
+
#
|
64
|
+
# @return [void]
|
65
|
+
def load_cache
|
66
|
+
cache = current_support_table_cache
|
67
|
+
return super if cache.nil?
|
68
|
+
|
69
|
+
find_each do |record|
|
70
|
+
support_table_cache_by_attributes.each do |attribute_names, case_sensitive|
|
71
|
+
attributes = record.attributes.select { |name, value| attribute_names.include?(name) }
|
72
|
+
cache_key = SupportTableCache.cache_key(self, attributes, attribute_names, case_sensitive)
|
73
|
+
cache.fetch(cache_key, expires_in: support_table_cache_ttl) { record }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Set a class-specific cache to use in lieu of the global cache.
|
79
|
+
#
|
80
|
+
# @param cache [ActiveSupport::Cache::Store, Symbol] The cache instance to use. You can also
|
81
|
+
# specify the value :memory to use an optimized in-memory cache.
|
82
|
+
# @return [void]
|
83
|
+
def support_table_cache=(cache)
|
84
|
+
cache = MemoryCache.new if cache == :memory
|
85
|
+
self.support_table_cache_impl = cache
|
86
|
+
end
|
87
|
+
|
22
88
|
protected
|
23
89
|
|
24
90
|
# Specify which attributes can be used for looking up records in the cache. Each value must
|
25
91
|
# define a unique key, Multiple unique keys can be specified.
|
92
|
+
#
|
26
93
|
# If multiple attributes are used to make up a unique key, then they should be passed in as an array.
|
27
|
-
#
|
94
|
+
#
|
95
|
+
# If you need to remove caching setup in a superclass, you can pass in the value false to reset
|
96
|
+
# cache behavior on the class.
|
97
|
+
#
|
98
|
+
# @param attributes [String, Symbol, Array<String, Symbol>, FalseClass] Attributes that make up a unique key.
|
28
99
|
# @param case_sensitive [Boolean] Indicate if strings should treated as case insensitive in the key.
|
29
|
-
|
100
|
+
# @param where [Hash] A hash representing a hard coded set of attributes that must match a query in order
|
101
|
+
# to cache the result. If a model has a default scope, then this value should be set to match the
|
102
|
+
# where clause in that scope.
|
103
|
+
# @return [void]
|
104
|
+
def cache_by(attributes, case_sensitive: true, where: nil)
|
105
|
+
if attributes == false
|
106
|
+
self.support_table_cache_by_attributes = []
|
107
|
+
return
|
108
|
+
end
|
109
|
+
|
30
110
|
attributes = Array(attributes).map(&:to_s).sort.freeze
|
31
|
-
|
111
|
+
|
112
|
+
if where
|
113
|
+
unless where.is_a?(Hash)
|
114
|
+
raise ArgumentError.new("where must be a Hash")
|
115
|
+
end
|
116
|
+
where = where.stringify_keys
|
117
|
+
end
|
118
|
+
|
119
|
+
self.support_table_cache_by_attributes ||= []
|
120
|
+
support_table_cache_by_attributes.delete_if { |data| data.first == attributes }
|
121
|
+
self.support_table_cache_by_attributes += [[attributes, case_sensitive, where]]
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def support_table_cache_disabled?
|
127
|
+
current_block_value = Thread.current.thread_variable_get("support_table_cache_disabled:#{name}")
|
128
|
+
if current_block_value.nil?
|
129
|
+
SupportTableCache.disabled?
|
130
|
+
else
|
131
|
+
current_block_value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def current_support_table_cache
|
136
|
+
return nil? if support_table_cache_disabled?
|
137
|
+
SupportTableCache.testing_cache || support_table_cache_impl || SupportTableCache.cache
|
32
138
|
end
|
33
139
|
end
|
34
140
|
|
35
141
|
class << self
|
36
|
-
# Disable the caching behavior. If a block is specified, then caching is only
|
37
|
-
# disabled for that block. If no block is specified, then caching is disabled
|
38
|
-
#
|
39
|
-
# @param disabled [Boolean] Caching will be disabled if this is true
|
142
|
+
# Disable the caching behavior for all classes. If a block is specified, then caching is only
|
143
|
+
# disabled for that block. If no block is specified, then caching is disabled globally.
|
144
|
+
#
|
145
|
+
# @param disabled [Boolean] Caching will be disabled if this is true and enabled if false.
|
146
|
+
# @yieldreturn The return value of the block.
|
40
147
|
def disable(disabled = true, &block)
|
41
148
|
if block
|
42
|
-
save_val = Thread.current
|
149
|
+
save_val = Thread.current.thread_variable_get(:support_table_cache_disabled)
|
43
150
|
begin
|
44
|
-
Thread.current
|
151
|
+
Thread.current.thread_variable_set(:support_table_cache_disabled, !!disabled)
|
45
152
|
ensure
|
46
|
-
Thread.current
|
153
|
+
Thread.current.thread_variable_set(:support_table_cache_disabled, save_val)
|
47
154
|
end
|
48
155
|
else
|
49
156
|
@disabled = !!disabled
|
50
157
|
end
|
51
158
|
end
|
52
159
|
|
160
|
+
# Enable the caching behavior for all classes. If a block is specified, then caching is only
|
161
|
+
# enabled for that block. If no block is specified, then caching is enabled globally.
|
162
|
+
#
|
163
|
+
# @yieldreturn The return value of the block.
|
53
164
|
def enable(&block)
|
54
165
|
disable(false, &block)
|
55
166
|
end
|
@@ -57,7 +168,7 @@ module SupportTableCache
|
|
57
168
|
# Return true if caching has been disabled.
|
58
169
|
# @return [Boolean]
|
59
170
|
def disabled?
|
60
|
-
block_value = Thread.current
|
171
|
+
block_value = Thread.current.thread_variable_get(:support_table_cache_disabled)
|
61
172
|
if block_value.nil?
|
62
173
|
!!(defined?(@disabled) && @disabled)
|
63
174
|
else
|
@@ -65,19 +176,63 @@ module SupportTableCache
|
|
65
176
|
end
|
66
177
|
end
|
67
178
|
|
68
|
-
|
179
|
+
# Set the global cache to use.
|
180
|
+
# @param value [ActiveSupport::Cache::Store, Symbol] The cache instance to use. You can also
|
181
|
+
# specify the value :memory to use an optimized in-memory cache.
|
182
|
+
# @return [void]
|
183
|
+
def cache=(value)
|
184
|
+
value = MemoryCache.new if value == :memory
|
185
|
+
@cache = value
|
186
|
+
end
|
69
187
|
|
188
|
+
# Get the global cache (will default to `Rails.cache` if running in a Rails environment).
|
189
|
+
#
|
190
|
+
# @return [ActiveSupport::Cache::Store]
|
70
191
|
def cache
|
71
|
-
if
|
192
|
+
if testing_cache
|
193
|
+
testing_cache
|
194
|
+
elsif defined?(@cache)
|
72
195
|
@cache
|
73
196
|
elsif defined?(Rails.cache)
|
74
197
|
Rails.cache
|
75
198
|
end
|
76
199
|
end
|
77
200
|
|
78
|
-
#
|
201
|
+
# Enter test mode for a block. New caches will be used within each test mode block. You
|
202
|
+
# can use this to wrap your test methods so that cached values from one test don't show up
|
203
|
+
# in subsequent tests.
|
204
|
+
#
|
205
|
+
# @return [void]
|
206
|
+
def testing!(&block)
|
207
|
+
save_val = Thread.current.thread_variable_get(:support_table_cache_test_cache)
|
208
|
+
if save_val.nil?
|
209
|
+
Thread.current.thread_variable_set(:support_table_cache_test_cache, MemoryCache.new)
|
210
|
+
end
|
211
|
+
begin
|
212
|
+
yield
|
213
|
+
ensure
|
214
|
+
Thread.current.thread_variable_set(:support_table_cache_test_cache, save_val)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Get the current test mode cache. This will only return a value inside of a `testing!` block.
|
219
|
+
#
|
220
|
+
# @return [SupportTableCache::MemoryCache]
|
221
|
+
# @api private
|
222
|
+
def testing_cache
|
223
|
+
unless defined?(@cache) && @cache.nil?
|
224
|
+
Thread.current.thread_variable_get(:support_table_cache_test_cache)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Generate a consistent cache key for a set of attributes. It will return nil if the attributes
|
79
229
|
# are not cacheable.
|
80
|
-
#
|
230
|
+
#
|
231
|
+
# @param klass [Class] The class that is being cached.
|
232
|
+
# @param attributes [Hash] The attributes used to find a record.
|
233
|
+
# @param key_attribute_names [Array] List of attributes that can be used as a key in the cache.
|
234
|
+
# @param case_sensitive [Boolean] Indicator if string values are case-sensitive in the cache key.
|
235
|
+
# @return [String]
|
81
236
|
# @api private
|
82
237
|
def cache_key(klass, attributes, key_attribute_names, case_sensitive)
|
83
238
|
return nil if attributes.blank? || key_attribute_names.blank?
|
@@ -98,33 +253,15 @@ module SupportTableCache
|
|
98
253
|
end
|
99
254
|
end
|
100
255
|
|
101
|
-
module FindByOverride
|
102
|
-
# Override for the find_by method that looks in the cache first.
|
103
|
-
def find_by(*args)
|
104
|
-
return super if SupportTableCache.cache.nil? || SupportTableCache.disabled?
|
105
|
-
|
106
|
-
cache_key = nil
|
107
|
-
attributes = args.first if args.size == 1 && args.first.is_a?(Hash)
|
108
|
-
if attributes
|
109
|
-
support_table_cache_by_attributes.each do |attribute_names, case_sensitive|
|
110
|
-
cache_key = SupportTableCache.cache_key(self, attributes, attribute_names, case_sensitive)
|
111
|
-
break if cache_key
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
if cache_key
|
116
|
-
SupportTableCache.cache.fetch(cache_key, expires_in: support_table_cache_ttl) { super }
|
117
|
-
else
|
118
|
-
super
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
256
|
# Remove the cache entry for this record.
|
257
|
+
#
|
124
258
|
# @return [void]
|
125
259
|
def uncache
|
126
260
|
cache_by_attributes = self.class.support_table_cache_by_attributes
|
127
|
-
return if cache_by_attributes.blank?
|
261
|
+
return if cache_by_attributes.blank?
|
262
|
+
|
263
|
+
cache = self.class.send(:current_support_table_cache)
|
264
|
+
return if cache.nil?
|
128
265
|
|
129
266
|
cache_by_attributes.each do |attribute_names, case_sensitive|
|
130
267
|
attributes = {}
|
@@ -132,7 +269,7 @@ module SupportTableCache
|
|
132
269
|
attributes[name] = self[name]
|
133
270
|
end
|
134
271
|
cache_key = SupportTableCache.cache_key(self.class, attributes, attribute_names, case_sensitive)
|
135
|
-
|
272
|
+
cache.delete(cache_key)
|
136
273
|
end
|
137
274
|
end
|
138
275
|
|
@@ -143,7 +280,10 @@ module SupportTableCache
|
|
143
280
|
# and after the change.
|
144
281
|
def support_table_clear_cache_entries
|
145
282
|
cache_by_attributes = self.class.support_table_cache_by_attributes
|
146
|
-
return if cache_by_attributes.blank?
|
283
|
+
return if cache_by_attributes.blank?
|
284
|
+
|
285
|
+
cache = self.class.send(:current_support_table_cache)
|
286
|
+
return if cache.nil?
|
147
287
|
|
148
288
|
cache_by_attributes.each do |attribute_names, case_sensitive|
|
149
289
|
attributes_before = {} if saved_change_to_id.blank? || saved_change_to_id.first.present?
|
@@ -158,7 +298,7 @@ module SupportTableCache
|
|
158
298
|
end
|
159
299
|
[attributes_before, attributes_after].compact.uniq.each do |attributes|
|
160
300
|
cache_key = SupportTableCache.cache_key(self.class, attributes, attribute_names, case_sensitive)
|
161
|
-
|
301
|
+
cache.delete(cache_key)
|
162
302
|
end
|
163
303
|
end
|
164
304
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: support_table_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Durand
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -50,6 +50,10 @@ files:
|
|
50
50
|
- README.md
|
51
51
|
- VERSION
|
52
52
|
- lib/support_table_cache.rb
|
53
|
+
- lib/support_table_cache/associations.rb
|
54
|
+
- lib/support_table_cache/find_by_override.rb
|
55
|
+
- lib/support_table_cache/memory_cache.rb
|
56
|
+
- lib/support_table_cache/relation_override.rb
|
53
57
|
- support_table_cache.gemspec
|
54
58
|
homepage: https://github.com/bdurand/support_table_cache
|
55
59
|
licenses:
|