support_table_cache 1.1.3 → 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 +4 -4
- data/ARCHITECTURE.md +386 -0
- data/CHANGELOG.md +6 -0
- data/README.md +1 -0
- data/VERSION +1 -1
- data/lib/support_table_cache/associations.rb +1 -1
- data/lib/support_table_cache/find_by_override.rb +5 -3
- data/lib/support_table_cache/memory_cache.rb +26 -0
- data/lib/support_table_cache/relation_override.rb +15 -4
- data/lib/support_table_cache.rb +18 -13
- metadata +4 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5102ba8b7a75ecc7186dc527880ff0278f9e259b020fc8b36b8e3e72c4b0c150
|
|
4
|
+
data.tar.gz: ebf2696d9ea3917895038b35989d2c23c5b418db4f405c4b36812e6da59f7691
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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: #<Status id: 1, name: 'active'>"]
|
|
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,12 @@ 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
|
+
|
|
7
13
|
## 1.1.3
|
|
8
14
|
|
|
9
15
|
### Fixed
|
data/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bdurand/support_table_cache/actions/workflows/continuous_integration.yml)
|
|
4
4
|
[](https://github.com/testdouble/standard)
|
|
5
|
+
[](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.
|
|
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
|
|
|
@@ -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
|
-
# @
|
|
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
|
-
# @
|
|
55
|
-
# @raise
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
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
|
-
# @
|
|
71
|
-
# @raise
|
|
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?
|
data/lib/support_table_cache.rb
CHANGED
|
@@ -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
|
-
# @
|
|
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
|
-
# @
|
|
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.
|
|
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
|
|
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>,
|
|
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
|
-
# @
|
|
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
|
-
# @
|
|
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
|
-
# @
|
|
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.
|
|
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:
|
|
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.
|
|
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: []
|