support_table_cache 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6c7814572a411ad62d5077cf255ff24059d2ab9292be81ded041de7ecff8a6ac
4
+ data.tar.gz: 015ea25fb5083ce1a80579a143486bf44a091fa375bf0d122b0889763321852c
5
+ SHA512:
6
+ metadata.gz: c4a1f00fa0873f0e28501119c5b588a6df166f5f9bcee84fcaf3dc4ffe0985251a6d6e6ee24f00d2ee740bd02b1dfe74db425a7a8170d8d5ddd98fbed0c8ecde
7
+ data.tar.gz: 81de8cda7452731a341e82752f3a25ab793c1b15e518c41c78118348841b41f5f8c56af13304e2565e9bfeffc00210764bf0351e4920d56af0e4e49704fdfde7
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 1.0.0
8
+
9
+ ### Added
10
+ - Add SupportTableCache concern to enable automatic caching on models when calling `find_by` with unique key parameters.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Brian Durand
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,102 @@
1
+ # Support Table Cache
2
+
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
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+
6
+ This gem adds caching for ActiveRecord support table models. These are models which have a unique key (i.e. a unique `name` attribute, etc.) and which have a limited number of entries (a few hundred at most). These are often models added do normalize the data structure.
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. This is most useful in situations where you have a unique key but need to get the database row for that key.
9
+
10
+ For instance, suppose you have a model `Status` that has a unique name attribute and you need to process a bunch of records from a data source that includes the status name. In order to do anything, you'll need to lookup each status by name to get the database id:
11
+
12
+ ```ruby
13
+ params.each do |data|
14
+ status = Status.find_by(name: data[:status]
15
+ Things.where(id: data[:id]).update!(status_id: status.id)
16
+ end
17
+ ```
18
+
19
+ With this gem, you can avoid the database query for the `find_by` call. You don't need to alter your code in any way other than to include `SupportTableCache` in your model and tell it which attributes comprise a unique key that can be used for caching.
20
+
21
+ ## Usage
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 insensitive strings, you need to specify that as well.
24
+
25
+ ```ruby
26
+ class MyModel < ApplicationRecord
27
+ include SupportTableCache
28
+
29
+ cache_by :id
30
+ cache_by [:group, :name], case_sensitive: false
31
+ end
32
+
33
+ # Uses cache
34
+ MyModel.find_by(id: 1)
35
+
36
+ # Uses cache on a composite key
37
+ MyModel.find_by(group: "first", name: "One")
38
+
39
+ # Does not use cache since value is not defined as a cacheable key
40
+ MyModel.find_by(value: 1)
41
+
42
+ # Does not use caching since not using find_by
43
+ MyModel.where(id: 1).first
44
+ ```
45
+
46
+ By default, records will be cleaned up from the cache only when they are modified. However, you can set a time to live on the model after which records will be removed from the cache.
47
+
48
+ ```ruby
49
+ class MyModel < ApplicationRecord
50
+ include SupportTableCache
51
+
52
+ self.support_table_cache_ttl = 5.minutes
53
+ end
54
+ ```
55
+
56
+ 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`` to use.
57
+
58
+ ```ruby
59
+ SupportTableCache.cache = ActiveSupport::Cache::MemoryStore.new
60
+ ```
61
+
62
+ You can also disable caching behavior entirely if you want or just within a block. You may want to disable it entirely in test mode if it interferes with your tests.
63
+
64
+ ```ruby
65
+ # Disable the cache globally
66
+ SupportTableCache.disable
67
+
68
+ SupportTableCache.enable do
69
+ # Re-enable the cache for the block
70
+ SupportTableCache.disable do
71
+ # Disable it again
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Installation
77
+
78
+ Add this line to your application's Gemfile:
79
+
80
+ ```ruby
81
+ gem "support_table_cache"
82
+ ```
83
+
84
+ And then execute:
85
+ ```bash
86
+ $ bundle
87
+ ```
88
+
89
+ Or install it yourself as:
90
+ ```bash
91
+ $ gem install support_table_cache
92
+ ```
93
+
94
+ ## Contributing
95
+
96
+ Open a pull request on [GitHub](https://github.com/bdurand/support_table_cache).
97
+
98
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
99
+
100
+ ## License
101
+
102
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This concern can be added to a model for a support table to add the ability to lookup
4
+ # entries in these table using Rails.cache when calling find_by rather than hitting the
5
+ # database every time.
6
+ module SupportTableCache
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :support_table_cache_by_attributes, instance_accessor: false
11
+ class_attribute :support_table_cache_ttl, instance_accessor: false
12
+
13
+ class << self
14
+ prepend FindByOverride unless include?(FindByOverride)
15
+ private :support_table_cache_by_attributes=
16
+ end
17
+
18
+ after_commit :support_table_clear_cache_entries
19
+ end
20
+
21
+ class_methods do
22
+ protected
23
+
24
+ # Specify which attributes can be used for looking up records in the cache. Each value must
25
+ # define a unique key, Multiple unique keys can be specified.
26
+ # If multiple attributes are used to make up a unique key, then they should be passed in as an array.
27
+ # @param attributes [String, Symbol, Array<String, Symbol>] Attributes that make up a unique key.
28
+ # @param case_sensitive [Boolean] Indicate if strings should treated as case insensitive in the key.
29
+ def cache_by(attributes, case_sensitive: true)
30
+ attributes = Array(attributes).map(&:to_s).sort.freeze
31
+ self.support_table_cache_by_attributes = (support_table_cache_by_attributes || []) + [[attributes, case_sensitive]]
32
+ end
33
+ end
34
+
35
+ 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
+ # globally.
39
+ # @param disabled [Boolean] Caching will be disabled if this is true, enabled if false.
40
+ def disable(disabled = true, &block)
41
+ if block
42
+ save_val = Thread.current[:support_table_cache_disabled]
43
+ begin
44
+ Thread.current[:support_table_cache_disabled] = !!disabled
45
+ ensure
46
+ Thread.current[:support_table_cache_disabled] = save_val
47
+ end
48
+ else
49
+ @disabled = !!disabled
50
+ end
51
+ end
52
+
53
+ def enable(&block)
54
+ disable(false, &block)
55
+ end
56
+
57
+ # Return true if caching has been disabled.
58
+ # @return [Boolean]
59
+ def disabled?
60
+ block_value = Thread.current[:support_table_cache_disabled]
61
+ if block_value.nil?
62
+ !!(defined?(@disabled) && @disabled)
63
+ else
64
+ block_value
65
+ end
66
+ end
67
+
68
+ attr_writer :cache
69
+
70
+ def cache
71
+ if defined?(@cache)
72
+ @cache
73
+ elsif defined?(Rails.cache)
74
+ Rails.cache
75
+ end
76
+ end
77
+
78
+ # Generate a consistent cache key for a set of attributes. Returns nil if the attributes
79
+ # are not cacheable.
80
+ # @param klass [Class] The class to
81
+ # @api private
82
+ def cache_key(klass, attributes, key_attribute_names, case_sensitive)
83
+ return nil if attributes.blank? || key_attribute_names.blank?
84
+
85
+ sorted_names = attributes.keys.map(&:to_s).sort
86
+ return nil unless sorted_names == key_attribute_names
87
+
88
+ sorted_attributes = {}
89
+ sorted_names.each do |attribute_name|
90
+ value = (attributes[attribute_name] || attributes[attribute_name.to_sym])
91
+ if !case_sensitive && (value.is_a?(String) || value.is_a?(Symbol))
92
+ value = value.to_s.downcase
93
+ end
94
+ sorted_attributes[attribute_name] = value
95
+ end
96
+
97
+ [klass.name, sorted_attributes]
98
+ end
99
+ end
100
+
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
+ # Remove the cache entry for this record.
124
+ # @return [void]
125
+ def uncache
126
+ cache_by_attributes = self.class.support_table_cache_by_attributes
127
+ return if cache_by_attributes.blank? || SupportTableCache.cache.nil?
128
+
129
+ cache_by_attributes.each do |attribute_names, case_sensitive|
130
+ attributes = {}
131
+ attribute_names.each do |name|
132
+ attributes[name] = self[name]
133
+ end
134
+ cache_key = SupportTableCache.cache_key(self.class, attributes, attribute_names, case_sensitive)
135
+ SupportTableCache.cache.delete(cache_key)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ # Clear all combinations of the cacheable attributes whenever any attribute changes.
142
+ # We have to make sure to clear the keys with the attribute values both before
143
+ # and after the change.
144
+ def support_table_clear_cache_entries
145
+ cache_by_attributes = self.class.support_table_cache_by_attributes
146
+ return if cache_by_attributes.blank? || SupportTableCache.cache.nil?
147
+
148
+ cache_by_attributes.each do |attribute_names, case_sensitive|
149
+ attributes_before = {} if saved_change_to_id.blank? || saved_change_to_id.first.present?
150
+ attributes_after = {} if saved_change_to_id.blank? || saved_change_to_id.last.present?
151
+ attribute_names.each do |name|
152
+ if attributes_before
153
+ attributes_before[name] = (saved_changes.include?(name) ? saved_changes[name].first : self[name])
154
+ end
155
+ if attributes_after
156
+ attributes_after[name] = self[name]
157
+ end
158
+ end
159
+ [attributes_before, attributes_after].compact.uniq.each do |attributes|
160
+ cache_key = SupportTableCache.cache_key(self.class, attributes, attribute_names, case_sensitive)
161
+ SupportTableCache.cache.delete(cache_key)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,33 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "support_table_cache"
3
+ spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
4
+ spec.authors = ["Brian Durand"]
5
+ spec.email = ["bbdurand@gmail.com"]
6
+
7
+ spec.summary = "Automatic ActiveRecord caching for small support tables."
8
+
9
+ spec.homepage = "https://github.com/bdurand/support_table_cache"
10
+ spec.license = "MIT"
11
+
12
+ # Specify which files should be added to the gem when it is released.
13
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
14
+ ignore_files = %w[
15
+ .
16
+ Appraisals
17
+ Gemfile
18
+ Gemfile.lock
19
+ Rakefile
20
+ bin/
21
+ gemfiles/
22
+ spec/
23
+ ]
24
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
26
+ end
27
+
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "activerecord"
31
+
32
+ spec.add_development_dependency "bundler"
33
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: support_table_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - bbdurand@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - VERSION
52
+ - lib/support_table_cache.rb
53
+ - support_table_cache.gemspec
54
+ homepage: https://github.com/bdurand/support_table_cache
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.0.3
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Automatic ActiveRecord caching for small support tables.
77
+ test_files: []