support_table_cache 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96d5ff63c1cd08f7389bf3b409d0afc4f706f42bed958c7b3a7f2b592d391453
4
- data.tar.gz: 76e59a449b9eeffd9df794f014abcbf324f4df2590e250817a4dd3b77467e5b5
3
+ metadata.gz: 5102ba8b7a75ecc7186dc527880ff0278f9e259b020fc8b36b8e3e72c4b0c150
4
+ data.tar.gz: ebf2696d9ea3917895038b35989d2c23c5b418db4f405c4b36812e6da59f7691
5
5
  SHA512:
6
- metadata.gz: 970762a6cb7aaf507552dce376efbcf1da64feb54b261fa315aaff2fc8898b4b8d3db755690c3c09e57e4c615ac7ae23e334e8d1426613b59a800a34c55f6b52
7
- data.tar.gz: deed3048a34f40d30aec7c902f09f265642e34a6e8108cca9db806929ab74ef57fd2f31d24b01dd892717a463e5c3bfdb1f6aaaf15b74d9b791d987560c2bc0f
6
+ metadata.gz: eae75a3e8afea4fcf9cff14fec02110ab0f332685bd912ca3f37fdd17baeeaa2db107824cbd0f6bec18acdf501815012476fd48f50d63b3a38c0767daae7cf98
7
+ data.tar.gz: 1be4a2c1013221d4af65eb26dbfd5e9c56eb130a4bdc94ce540854f11a3df7dd7526e264ba645f7e46340cd43de8bef8899837043031aeae8897e463113e4fc5
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,386 @@
1
+ # Support Table Cache Architecture
2
+
3
+ This document describes the architecture and design of the `support_table_cache` gem, which provides automatic caching for ActiveRecord support table models.
4
+
5
+ ## Overview
6
+
7
+ Support Table Cache is designed to optimize database queries for small lookup tables (support tables) that have:
8
+ - Unique keys (e.g., unique `name` attribute)
9
+ - Limited number of entries (a few hundred at most)
10
+ - Rarely updated data but frequently queried
11
+ - Used for data normalization (lookup tables)
12
+
13
+ The gem automatically caches records when using `find_by` methods and `belongs_to` associations, eliminating redundant database queries.
14
+
15
+ ## High-Level Architecture
16
+
17
+ ```mermaid
18
+ flowchart TB
19
+ subgraph "Application Layer"
20
+ App["Application Code"]
21
+ Models["ActiveRecord Models"]
22
+ end
23
+
24
+ subgraph "Support Table Cache Gem"
25
+ STC["SupportTableCache Module"]
26
+ Assoc["Associations Module"]
27
+ FindBy["FindByOverride"]
28
+ RelOverride["RelationOverride"]
29
+ MemCache["MemoryCache"]
30
+ end
31
+
32
+ subgraph "Cache Layer"
33
+ RC["Rails.cache / Custom Cache"]
34
+ MC["In-Memory Cache"]
35
+ end
36
+
37
+ subgraph "Database Layer"
38
+ DB["PostgreSQL/MySQL/SQLite"]
39
+ end
40
+
41
+ App --> Models
42
+ Models --> STC
43
+ STC --> FindBy
44
+ STC --> Assoc
45
+ STC --> RelOverride
46
+ STC --> MemCache
47
+
48
+ FindBy --> RC
49
+ FindBy --> MC
50
+ Assoc --> RC
51
+ Assoc --> MC
52
+ RelOverride --> RC
53
+ RelOverride --> MC
54
+
55
+ FindBy -.-> DB
56
+ Assoc -.-> DB
57
+ RelOverride -.-> DB
58
+
59
+ RC -.-> DB
60
+ MC -.-> DB
61
+
62
+ classDef appLayer fill:#e1f5fe
63
+ classDef cacheLayer fill:#fff3e0
64
+ classDef dbLayer fill:#f3e5f5
65
+ classDef gemLayer fill:#e8f5e8
66
+
67
+ class App,Models appLayer
68
+ class STC,Assoc,FindBy,RelOverride,MemCache gemLayer
69
+ class RC,MC cacheLayer
70
+ class DB dbLayer
71
+ ```
72
+
73
+ ## Core Components
74
+
75
+ ### 1. SupportTableCache Module
76
+
77
+ The main module that provides caching functionality to ActiveRecord models.
78
+
79
+ ```mermaid
80
+ flowchart LR
81
+ subgraph "SupportTableCache Module"
82
+ CM["Class Methods"]
83
+ IM["Instance Methods"]
84
+ CC["Cache Configuration"]
85
+ CCE["Cache Control & Expiry"]
86
+ end
87
+
88
+ subgraph "Class Methods"
89
+ CB["cache_by()"]
90
+ DC["disable_cache()"]
91
+ EC["enable_cache()"]
92
+ LC["load_cache()"]
93
+ SC["support_table_cache="]
94
+ end
95
+
96
+ subgraph "Instance Methods"
97
+ UC["uncache()"]
98
+ ClearCache["support_table_clear_cache_entries()"]
99
+ end
100
+
101
+ CM --> CB
102
+ CM --> DC
103
+ CM --> EC
104
+ CM --> LC
105
+ CM --> SC
106
+
107
+ IM --> UC
108
+ IM --> ClearCache
109
+
110
+ CB --> CC
111
+ SC --> CC
112
+ UC --> CCE
113
+ ClearCache --> CCE
114
+ ```
115
+
116
+ ### 2. Cache Key Generation Flow
117
+
118
+ ```mermaid
119
+ sequenceDiagram
120
+ participant App as Application
121
+ participant Model as Model.find_by
122
+ participant Override as FindByOverride
123
+ participant Cache as Cache Store
124
+ participant DB as Database
125
+
126
+ App->>Model: Model.find_by(name: "example")
127
+ Model->>Override: Intercept find_by call
128
+
129
+ Override->>Override: Extract attributes from query
130
+ Override->>Override: Check cache_by_attributes config
131
+ Override->>Override: Generate cache key from attributes
132
+
133
+ alt Cache Hit
134
+ Override->>Cache: fetch(cache_key)
135
+ Cache-->>Override: Return cached record
136
+ Override-->>App: Return record
137
+ else Cache Miss
138
+ Override->>Cache: fetch(cache_key) with block
139
+ Cache->>DB: Execute original find_by query
140
+ DB-->>Cache: Return record from DB
141
+ Cache->>Cache: Store record with TTL
142
+ Cache-->>Override: Return record
143
+ Override-->>App: Return record
144
+ end
145
+ ```
146
+
147
+ ### 3. Cache Key Structure
148
+
149
+ ```mermaid
150
+ flowchart TD
151
+ Attrs["Query Attributes<br/>{name: 'active', type: 'primary'}"]
152
+
153
+ subgraph "Key Generation Process"
154
+ Sort["Sort attribute names<br/>['name', 'type']"]
155
+ CaseCheck["Apply case sensitivity<br/>name: 'active' → 'active'<br/>type: 'primary' → 'primary'"]
156
+ KeyGen["Generate cache key<br/>['ModelName', {name: 'active', type: 'primary'}]"]
157
+ end
158
+
159
+ Attrs --> Sort
160
+ Sort --> CaseCheck
161
+ CaseCheck --> KeyGen
162
+
163
+ KeyGen --> CacheStore["Cache Store<br/>Key: ['Status', {name: 'active', type: 'primary'}]<br/>Value: #&lt;Status id: 1, name: 'active'&gt;"]
164
+ ```
165
+
166
+ ### 4. Association Caching Flow
167
+
168
+ ```mermaid
169
+ sequenceDiagram
170
+ participant App as Application
171
+ participant Parent as Parent Model
172
+ participant Assoc as Association Reader
173
+ participant Cache as Cache Store
174
+ participant Child as Child Model
175
+ participant DB as Database
176
+
177
+ App->>Parent: parent.status
178
+ Parent->>Assoc: Call association reader
179
+
180
+ Assoc->>Assoc: Extract foreign key value
181
+ Assoc->>Assoc: Build cache key from foreign key
182
+
183
+ alt Cache Hit
184
+ Assoc->>Cache: fetch(cache_key)
185
+ Cache-->>Assoc: Return cached record
186
+ Assoc-->>App: Return associated record
187
+ else Cache Miss
188
+ Assoc->>Cache: fetch(cache_key) with block
189
+ Cache->>Child: Load association normally
190
+ Child->>DB: Query database
191
+ DB-->>Child: Return record
192
+ Child-->>Cache: Return record
193
+ Cache->>Cache: Store record with TTL
194
+ Cache-->>Assoc: Return record
195
+ Assoc-->>App: Return associated record
196
+ end
197
+ ```
198
+
199
+ ### 5. Cache Invalidation Strategy
200
+
201
+ ```mermaid
202
+ flowchart TD
203
+ subgraph "Record Lifecycle Events"
204
+ Create["Record Created"]
205
+ Update["Record Updated"]
206
+ Delete["Record Deleted"]
207
+ end
208
+
209
+ subgraph "Cache Invalidation Process"
210
+ Hook["after_commit callback"]
211
+ ExtractKeys["Extract all cacheable<br/>attribute combinations"]
212
+ BuildKeys["Build cache keys for<br/>before & after states"]
213
+ ClearCache["Delete cache entries"]
214
+ end
215
+
216
+ subgraph "Cache Keys Cleared"
217
+ BeforeKeys["Keys with old values"]
218
+ AfterKeys["Keys with new values"]
219
+ end
220
+
221
+ Create --> Hook
222
+ Update --> Hook
223
+ Delete --> Hook
224
+
225
+ Hook --> ExtractKeys
226
+ ExtractKeys --> BuildKeys
227
+ BuildKeys --> ClearCache
228
+
229
+ ClearCache --> BeforeKeys
230
+ ClearCache --> AfterKeys
231
+ ```
232
+
233
+ ### 6. Cache Implementation Types
234
+
235
+ ```mermaid
236
+ flowchart LR
237
+ subgraph "Cache Types"
238
+ subgraph "External Cache"
239
+ RC["Rails.cache<br/>(Redis/Memcached)"]
240
+ CC["Custom Cache Store"]
241
+ end
242
+
243
+ subgraph "In-Memory Cache"
244
+ MC["MemoryCache<br/>(Process-local)"]
245
+ end
246
+ end
247
+
248
+ subgraph "Configuration"
249
+ Global["Global Cache<br/>SupportTableCache.cache = store"]
250
+ PerClass["Per-Class Cache<br/>Model.support_table_cache = :memory"]
251
+ Testing["Test Mode<br/>SupportTableCache.testing!"]
252
+ end
253
+
254
+ Global --> RC
255
+ Global --> CC
256
+ PerClass --> MC
257
+ Testing --> MC
258
+
259
+ subgraph "Trade-offs"
260
+ ExtPros["✓ Shared across processes<br/>✓ Automatic invalidation<br/>✓ TTL support"]
261
+ ExtCons["✗ Network overhead<br/>✗ Serialization cost"]
262
+
263
+ MemPros["✓ Ultra-fast access<br/>✓ No network overhead<br/>✓ No serialization"]
264
+ MemCons["✗ Per-process storage<br/>✗ Manual invalidation<br/>✗ Memory usage"]
265
+ end
266
+
267
+ RC -.-> ExtPros
268
+ CC -.-> ExtPros
269
+ RC -.-> ExtCons
270
+ CC -.-> ExtCons
271
+
272
+ MC -.-> MemPros
273
+ MC -.-> MemCons
274
+ ```
275
+
276
+ ### 7. Testing Integration
277
+
278
+ ```mermaid
279
+ flowchart TD
280
+ subgraph "Test Execution"
281
+ TestStart["Test Begins"]
282
+ TestCode["Test Code Execution"]
283
+ TestEnd["Test Ends"]
284
+ end
285
+
286
+ subgraph "Cache Isolation"
287
+ TestCache["Isolated Test Cache<br/>(MemoryCache per test)"]
288
+ CleanSlate["Clean State<br/>(No cache pollution)"]
289
+ end
290
+
291
+ TestStart --> TestCache
292
+ TestCache --> TestCode
293
+ TestCode --> CleanSlate
294
+ CleanSlate --> TestEnd
295
+
296
+ subgraph "Integration Pattern"
297
+ RSpecWrap["RSpec around hook<br/>SupportTableCache.testing!"]
298
+ MiniTestWrap["MiniTest around hook<br/>SupportTableCache.testing!"]
299
+ end
300
+
301
+ RSpecWrap -.-> TestCache
302
+ MiniTestWrap -.-> TestCache
303
+ ```
304
+
305
+ ## Configuration Patterns
306
+
307
+ ### Model Setup
308
+
309
+ ```ruby
310
+ class Status < ApplicationRecord
311
+ include SupportTableCache
312
+
313
+ # Cache by single unique attribute
314
+ cache_by :name, case_sensitive: false
315
+
316
+ # Cache by composite unique key
317
+ cache_by [:group, :name]
318
+
319
+ # Cache by id (for associations)
320
+ cache_by :id
321
+
322
+ # Optional: Set TTL for cache entries
323
+ self.support_table_cache_ttl = 5.minutes
324
+
325
+ # Optional: Use in-memory cache
326
+ self.support_table_cache = :memory
327
+ end
328
+ ```
329
+
330
+ ### Association Setup
331
+
332
+ ```ruby
333
+ class Order < ApplicationRecord
334
+ include SupportTableCache::Associations
335
+
336
+ belongs_to :status
337
+ cache_belongs_to :status
338
+ end
339
+ ```
340
+
341
+ ## Performance Benefits
342
+
343
+ ### Query Elimination
344
+
345
+ ```mermaid
346
+ sequenceDiagram
347
+ participant App as Application
348
+ participant Cache as Cache
349
+ participant DB as Database
350
+
351
+ Note over App,DB: Without Cache
352
+ App->>DB: Status.find_by(name: 'active')
353
+ DB-->>App: Record
354
+ App->>DB: Status.find_by(name: 'active')
355
+ DB-->>App: Same Record (redundant query)
356
+ App->>DB: Status.find_by(name: 'active')
357
+ DB-->>App: Same Record (redundant query)
358
+
359
+ Note over App,DB: With Cache
360
+ App->>Cache: Status.find_by(name: 'active')
361
+ Cache->>DB: First query only
362
+ DB-->>Cache: Record
363
+ Cache-->>App: Record
364
+ App->>Cache: Status.find_by(name: 'active')
365
+ Cache-->>App: Record (from cache)
366
+ App->>Cache: Status.find_by(name: 'active')
367
+ Cache-->>App: Record (from cache)
368
+ ```
369
+
370
+ ## Design Principles
371
+
372
+ 1. **Transparent Integration**: No code changes required beyond configuration
373
+ 2. **Selective Caching**: Only caches queries that match configured unique keys
374
+ 3. **Automatic Invalidation**: Cache entries are cleared when records change
375
+ 4. **Flexible Cache Backends**: Supports various cache stores including in-memory
376
+ 5. **Test Isolation**: Provides testing utilities to prevent cache pollution
377
+ 6. **Performance Optimization**: Minimizes database queries for frequently accessed lookup data
378
+
379
+ ## Use Cases
380
+
381
+ - **Status/Type Tables**: Small enums stored in database tables
382
+ - **Configuration Tables**: Application settings and parameters
383
+ - **Reference Data**: Countries, states, categories, etc.
384
+ - **Lookup Tables**: Any small, rarely-changing reference data
385
+
386
+ This architecture enables significant performance improvements for applications that heavily query small support tables while maintaining data consistency and providing flexible caching options.
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ 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.4
8
+
9
+ ### Fixed
10
+
11
+ - Fixed issue where using `find_by` on a `has_many` relation would not take the scope of the relation into account when looking up the cached record. Now chaining a `find_by` onto a `has_many` relation will correctly bypass the cache and directly query the database.
12
+
13
+ ## 1.1.3
14
+
15
+ ### Fixed
16
+ - Avoid calling methods that require a database connection when setting up belongs to caching.
17
+
7
18
  ## 1.1.2
8
19
 
9
20
  ### Fixed
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
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
+ [![Gem Version](https://badge.fury.io/rb/support_table_cache.svg)](https://badge.fury.io/rb/support_table_cache)
5
6
 
6
7
  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
8
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 1.1.4
@@ -14,7 +14,7 @@ module SupportTableCache
14
14
  #
15
15
  # @param association_name [Symbol, String] The association name to cache.
16
16
  # @return [void]
17
- # @raise ArgumentError if the association is not defined or if it has a runtime scope.
17
+ # @raise [ArgumentError] if the association is not defined or if it has a runtime scope.
18
18
  def cache_belongs_to(association_name)
19
19
  reflection = reflections[association_name.to_s]
20
20
 
@@ -26,13 +26,21 @@ module SupportTableCache
26
26
  raise ArgumentError.new("Cannot cache belongs_to #{association_name} association because it has a scope")
27
27
  end
28
28
 
29
+ define_belongs_to_with_cache(association_name.to_s)
30
+ end
31
+
32
+ private
33
+
34
+ def define_belongs_to_with_cache(association_name)
29
35
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
30
36
  def #{association_name}_with_cache
31
- foreign_key = self.send(#{reflection.foreign_key.inspect})
37
+ reflection = self.class.reflections[#{association_name.inspect}]
38
+ foreign_key = self.send(reflection.foreign_key)
32
39
  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)
40
+
41
+ key = [reflection.class_name, {reflection.association_primary_key => foreign_key}]
42
+ cache = reflection.klass.send(:current_support_table_cache)
43
+ ttl = reflection.klass.send(:support_table_cache_ttl)
36
44
  if cache
37
45
  cache.fetch(key, expires_in: ttl) do
38
46
  #{association_name}_without_cache
@@ -39,7 +39,8 @@ module SupportTableCache
39
39
  # Same as find_by, but performs a safety check to confirm the query will hit the cache.
40
40
  #
41
41
  # @param attributes [Hash] Attributes to find the record by.
42
- # @raise ArgumentError if the query cannot use the cache.
42
+ # @return [ActiveRecord::Base, nil] The found record or nil if not found.
43
+ # @raise [ArgumentError] if the query cannot use the cache.
43
44
  def fetch_by(attributes)
44
45
  find_by_attribute_names = support_table_find_by_attribute_names(attributes)
45
46
  unless support_table_cache_by_attributes.any? { |attribute_names, _ci, _where| attribute_names == find_by_attribute_names }
@@ -51,8 +52,9 @@ module SupportTableCache
51
52
  # Same as find_by!, but performs a safety check to confirm the query will hit the cache.
52
53
  #
53
54
  # @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.
55
+ # @return [ActiveRecord::Base] The found record.
56
+ # @raise [ArgumentError] if the query cannot use the cache.
57
+ # @raise [ActiveRecord::RecordNotFound] if no record is found.
56
58
  def fetch_by!(attributes)
57
59
  value = fetch_by(attributes)
58
60
  if value.nil?
@@ -8,11 +8,20 @@ module SupportTableCache
8
8
  # This cache will not store nil values. This is to prevent the cache from filling up with
9
9
  # cache misses because there is no purging mechanism.
10
10
  class MemoryCache
11
+ # Create a new memory cache.
12
+ #
13
+ # @return [SupportTableCache::MemoryCache]
11
14
  def initialize
12
15
  @cache = {}
13
16
  @mutex = Mutex.new
14
17
  end
15
18
 
19
+ # Fetch a value from the cache. If the key is not found or has expired, yields to get a new value.
20
+ #
21
+ # @param key [Object] The cache key.
22
+ # @param expires_in [Integer, nil] Time in seconds until the cached value expires.
23
+ # @yield Block to execute to get a new value if the key is not cached.
24
+ # @return [Object, nil] The cached value or the result of the block, or nil if no value is found.
16
25
  def fetch(key, expires_in: nil)
17
26
  serialized_value, expire_at = @cache[key]
18
27
  if serialized_value.nil? || (expire_at && expire_at < Process.clock_gettime(Process::CLOCK_MONOTONIC))
@@ -24,10 +33,20 @@ module SupportTableCache
24
33
  Marshal.load(serialized_value)
25
34
  end
26
35
 
36
+ # Read a value from the cache.
37
+ #
38
+ # @param key [Object] The cache key.
39
+ # @return [Object, nil] The cached value or nil if not found.
27
40
  def read(key)
28
41
  fetch(key)
29
42
  end
30
43
 
44
+ # Write a value to the cache.
45
+ #
46
+ # @param key [Object] The cache key.
47
+ # @param value [Object] The value to cache. Nil values are not cached.
48
+ # @param expires_in [Integer, nil] Time in seconds until the cached value expires.
49
+ # @return [void]
31
50
  def write(key, value, expires_in: nil)
32
51
  return if value.nil?
33
52
 
@@ -42,10 +61,17 @@ module SupportTableCache
42
61
  end
43
62
  end
44
63
 
64
+ # Delete a value from the cache.
65
+ #
66
+ # @param key [Object] The cache key.
67
+ # @return [void]
45
68
  def delete(key)
46
69
  @cache.delete(key)
47
70
  end
48
71
 
72
+ # Clear all values from the cache.
73
+ #
74
+ # @return [void]
49
75
  def clear
50
76
  @cache.clear
51
77
  end
@@ -5,12 +5,18 @@ module SupportTableCache
5
5
 
6
6
  module RelationOverride
7
7
  # Override for the find_by method that looks in the cache first.
8
+ #
9
+ # @param args [Array<Object>] Arguments passed to find_by.
10
+ # @return [ActiveRecord::Base, nil] The found record or nil if not found.
8
11
  def find_by(*args)
9
12
  return super unless klass.include?(SupportTableCache)
10
13
 
11
14
  cache = klass.send(:current_support_table_cache)
12
15
  return super unless cache
13
16
 
17
+ # Skip caching for has_many or has_many :through associations
18
+ return super if is_a?(ActiveRecord::Associations::CollectionProxy)
19
+
14
20
  return super if select_values.present?
15
21
 
16
22
  cache_key = nil
@@ -43,7 +49,10 @@ module SupportTableCache
43
49
  end
44
50
 
45
51
  # Override for the find_by! method that looks in the cache first.
46
- # @raise ActiveRecord::RecordNotFound if no record is found.
52
+ #
53
+ # @param args [Array<Object>] Arguments passed to find_by!.
54
+ # @return [ActiveRecord::Base] The found record.
55
+ # @raise [ActiveRecord::RecordNotFound] if no record is found.
47
56
  def find_by!(*args)
48
57
  value = find_by(*args)
49
58
  unless value
@@ -55,7 +64,8 @@ module SupportTableCache
55
64
  # Same as find_by, but performs a safety check to confirm the query will hit the cache.
56
65
  #
57
66
  # @param attributes [Hash] Attributes to find the record by.
58
- # @raise ArgumentError if the query cannot use the cache.
67
+ # @return [ActiveRecord::Base, nil] The found record or nil if not found.
68
+ # @raise [ArgumentError] if the query cannot use the cache.
59
69
  def fetch_by(attributes)
60
70
  find_by_attribute_names = support_table_find_by_attribute_names(attributes)
61
71
  unless klass.support_table_cache_by_attributes.any? { |attribute_names, _ci| attribute_names == find_by_attribute_names }
@@ -67,8 +77,9 @@ module SupportTableCache
67
77
  # Same as find_by!, but performs a safety check to confirm the query will hit the cache.
68
78
  #
69
79
  # @param attributes [Hash] Attributes to find the record by.
70
- # @raise ArgumentError if the query cannot use the cache.
71
- # @raise ActiveRecord::RecordNotFound if no record is found.
80
+ # @return [ActiveRecord::Base] The found record.
81
+ # @raise [ArgumentError] if the query cannot use the cache.
82
+ # @raise [ActiveRecord::RecordNotFound] if no record is found.
72
83
  def fetch_by!(attributes)
73
84
  value = fetch_by(attributes)
74
85
  if value.nil?
@@ -39,7 +39,8 @@ module SupportTableCache
39
39
  # for a class will always take precedence over the global setting.
40
40
  #
41
41
  # @param disabled [Boolean] Caching will be disabled if this is true and enabled if false.
42
- # @yieldreturn The return value of the block.
42
+ # @yield Executes the provided block with caching disabled or enabled.
43
+ # @return [Object] The return value of the block.
43
44
  def disable_cache(disabled = true, &block)
44
45
  varname = "support_table_cache_disabled:#{name}"
45
46
  save_val = Thread.current.thread_variable_get(varname)
@@ -54,7 +55,8 @@ module SupportTableCache
54
55
  # Enable the caching behavior for this class within the block. The enabled setting
55
56
  # for a class will always take precedence over the global setting.
56
57
  #
57
- # @yieldreturn The return value of the block.
58
+ # @yield Executes the provided block with caching enabled.
59
+ # @return [Object] The return value of the block.
58
60
  def enable_cache(&block)
59
61
  disable_cache(false, &block)
60
62
  end
@@ -69,7 +71,7 @@ module SupportTableCache
69
71
 
70
72
  find_each do |record|
71
73
  support_table_cache_by_attributes.each do |attribute_names, case_sensitive|
72
- attributes = record.attributes.select { |name, value| attribute_names.include?(name) }
74
+ attributes = record.attributes.slice(*attribute_names)
73
75
  cache_key = SupportTableCache.cache_key(self, attributes, attribute_names, case_sensitive)
74
76
  cache.fetch(cache_key, expires_in: support_table_cache_ttl) { record }
75
77
  end
@@ -89,16 +91,16 @@ module SupportTableCache
89
91
  protected
90
92
 
91
93
  # Specify which attributes can be used for looking up records in the cache. Each value must
92
- # define a unique key, Multiple unique keys can be specified.
94
+ # define a unique key. Multiple unique keys can be specified.
93
95
  #
94
96
  # If multiple attributes are used to make up a unique key, then they should be passed in as an array.
95
97
  #
96
98
  # If you need to remove caching setup in a superclass, you can pass in the value false to reset
97
99
  # cache behavior on the class.
98
100
  #
99
- # @param attributes [String, Symbol, Array<String, Symbol>, FalseClass] Attributes that make up a unique key.
100
- # @param case_sensitive [Boolean] Indicate if strings should treated as case insensitive in the key.
101
- # @param where [Hash] A hash representing a hard coded set of attributes that must match a query in order
101
+ # @param attributes [String, Symbol, Array<String, Symbol>, false] Attributes that make up a unique key.
102
+ # @param case_sensitive [Boolean] Indicate if strings should be treated as case insensitive in the key.
103
+ # @param where [Hash, nil] A hash representing a hard coded set of attributes that must match a query in order
102
104
  # to cache the result. If a model has a default scope, then this value should be set to match the
103
105
  # where clause in that scope.
104
106
  # @return [void]
@@ -144,7 +146,8 @@ module SupportTableCache
144
146
  # disabled for that block. If no block is specified, then caching is disabled globally.
145
147
  #
146
148
  # @param disabled [Boolean] Caching will be disabled if this is true and enabled if false.
147
- # @yieldreturn The return value of the block.
149
+ # @yield Executes the provided block with caching disabled or enabled (if block is given).
150
+ # @return [Object, nil] The return value of the block if a block is given, nil otherwise.
148
151
  def disable(disabled = true, &block)
149
152
  if block
150
153
  save_val = Thread.current.thread_variable_get(:support_table_cache_disabled)
@@ -162,7 +165,8 @@ module SupportTableCache
162
165
  # Enable the caching behavior for all classes. If a block is specified, then caching is only
163
166
  # enabled for that block. If no block is specified, then caching is enabled globally.
164
167
  #
165
- # @yieldreturn The return value of the block.
168
+ # @yield Executes the provided block with caching enabled (if block is given).
169
+ # @return [Object, nil] The return value of the block if a block is given, nil otherwise.
166
170
  def enable(&block)
167
171
  disable(false, &block)
168
172
  end
@@ -204,7 +208,8 @@ module SupportTableCache
204
208
  # can use this to wrap your test methods so that cached values from one test don't show up
205
209
  # in subsequent tests.
206
210
  #
207
- # @return [void]
211
+ # @yield Executes the provided block in test mode.
212
+ # @return [Object] The return value of the block.
208
213
  def testing!(&block)
209
214
  save_val = Thread.current.thread_variable_get(:support_table_cache_test_cache)
210
215
  if save_val.nil?
@@ -219,7 +224,7 @@ module SupportTableCache
219
224
 
220
225
  # Get the current test mode cache. This will only return a value inside of a `testing!` block.
221
226
  #
222
- # @return [SupportTableCache::MemoryCache]
227
+ # @return [SupportTableCache::MemoryCache, nil] The test cache or nil if not in test mode.
223
228
  # @api private
224
229
  def testing_cache
225
230
  unless defined?(@cache) && @cache.nil?
@@ -232,9 +237,9 @@ module SupportTableCache
232
237
  #
233
238
  # @param klass [Class] The class that is being cached.
234
239
  # @param attributes [Hash] The attributes used to find a record.
235
- # @param key_attribute_names [Array] List of attributes that can be used as a key in the cache.
240
+ # @param key_attribute_names [Array<String>] List of attributes that can be used as a key in the cache.
236
241
  # @param case_sensitive [Boolean] Indicator if string values are case-sensitive in the cache key.
237
- # @return [String]
242
+ # @return [Array(String, Hash), nil] A two-element array with the class name and attributes hash, or nil if not cacheable.
238
243
  # @api private
239
244
  def cache_key(klass, attributes, key_attribute_names, case_sensitive)
240
245
  return nil if attributes.blank? || key_attribute_names.blank?
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: support_table_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-12-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -38,13 +37,13 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
- description:
42
40
  email:
43
41
  - bbdurand@gmail.com
44
42
  executables: []
45
43
  extensions: []
46
44
  extra_rdoc_files: []
47
45
  files:
46
+ - ARCHITECTURE.md
48
47
  - CHANGELOG.md
49
48
  - MIT-LICENSE
50
49
  - README.md
@@ -59,7 +58,6 @@ homepage: https://github.com/bdurand/support_table_cache
59
58
  licenses:
60
59
  - MIT
61
60
  metadata: {}
62
- post_install_message:
63
61
  rdoc_options: []
64
62
  require_paths:
65
63
  - lib
@@ -74,8 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
72
  - !ruby/object:Gem::Version
75
73
  version: '0'
76
74
  requirements: []
77
- rubygems_version: 3.0.3
78
- signing_key:
75
+ rubygems_version: 3.6.9
79
76
  specification_version: 4
80
77
  summary: Automatic ActiveRecord caching for small support tables.
81
78
  test_files: []