My opinionated ruby on rails template
1# frozen_string_literal: true
2
3say 'Setting up caching strategies...', :green
4
5gem 'identity_cache'
6gem 'cityhash' # Required for identity_cache
7
8say ' Configuring IdentityCache...', :cyan
9initializer 'identity_cache.rb', <<~RUBY
10 # frozen_string_literal: true
11
12 # IdentityCache - Blob level caching for Active Record
13 # See: https://github.com/Shopify/identity_cache
14
15 IdentityCache.logger = Rails.logger
16 IdentityCache.cache_backend = Rails.cache
17RUBY
18
19say ' Creating Cacheable concern...', :cyan
20file 'app/models/concerns/cacheable.rb', <<~RUBY
21 # frozen_string_literal: true
22
23 # Include in models for identity caching
24 #
25 # Usage:
26 # class User < ApplicationRecord
27 # include Cacheable
28 #
29 # cache_index :email
30 # cache_has_many :posts
31 # end
32 #
33 # User.fetch(1) # Cached fetch by id
34 # User.fetch_by_email(email) # Cached fetch by index
35 #
36 module Cacheable
37 extend ActiveSupport::Concern
38
39 included do
40 include IdentityCache
41 end
42
43 class_methods do
44 # Expire cache when record is updated
45 def expire_cache_for(record)
46 record.expire_cache if record.respond_to?(:expire_cache)
47 end
48 end
49 end
50RUBY
51
52say ' Creating CacheHelper...', :cyan
53file 'app/helpers/cache_helper.rb', <<~RUBY
54 # frozen_string_literal: true
55
56 # Helper methods for view caching
57 #
58 # Russian Doll Caching:
59 # The key insight is that when a nested object changes,
60 # only its cache (and parent caches) are invalidated.
61 #
62 # Example view structure:
63 # <% cache @project do %>
64 # <%= render @project.tasks %>
65 # <% end %>
66 #
67 # # _task.html.erb
68 # <% cache task do %>
69 # <%= task.name %>
70 # <%= render task.comments %>
71 # <% end %>
72 #
73 module CacheHelper
74 # Cache with automatic expiry based on record updated_at
75 # Also includes a version number for manual cache busting
76 #
77 # Usage:
78 # <% cache_with_version @user, 'v2' do %>
79 # ...
80 # <% end %>
81 #
82 def cache_with_version(record, version = 'v1', options = {}, &block)
83 cache([version, record], options, &block)
84 end
85
86 # Cache a collection with automatic key generation
87 # Useful for lists that change frequently
88 #
89 # Usage:
90 # <% cache_collection @users do |user| %>
91 # <%= render user %>
92 # <% end %>
93 #
94 def cache_collection(collection, options = {}, &block)
95 cache([collection.cache_key_with_version, collection.size], options, &block)
96 end
97
98 # Time-based cache for content that should refresh periodically
99 #
100 # Usage:
101 # <% cache_for 5.minutes, 'dashboard_stats' do %>
102 # ...
103 # <% end %>
104 #
105 def cache_for(duration, key, options = {}, &block)
106 expires_key = (Time.current.to_i / duration.to_i)
107 cache([key, expires_key], options, &block)
108 end
109 end
110RUBY
111
112say ' Creating CacheWarmer job...', :cyan
113file 'app/jobs/cache_warmer_job.rb', <<~RUBY
114 # frozen_string_literal: true
115
116 # Warm caches for frequently accessed data
117 #
118 # Schedule this job to run periodically:
119 # CacheWarmerJob.perform_later
120 #
121 # Or warm specific caches:
122 # CacheWarmerJob.perform_later(caches: ['users', 'settings'])
123 #
124 class CacheWarmerJob < ApplicationJob
125 queue_as :low
126
127 def perform(caches: nil)
128 warmers = caches || default_warmers
129 warmers.each { |warmer| send("warm_\#{warmer}") }
130 end
131
132 private
133
134 def default_warmers
135 %w[settings]
136 end
137
138 # Add your cache warming methods here
139 #
140 # def warm_users
141 # User.active.find_each do |user|
142 # Rails.cache.fetch(user.cache_key_with_version) { user }
143 # end
144 # end
145
146 def warm_settings
147 # Example: warm application settings
148 # AppConfig.all.each do |config|
149 # Rails.cache.fetch(['app_config', config.key]) { config.value }
150 # end
151 end
152 end
153RUBY
154
155say ' Documenting caching patterns...', :cyan
156file 'docs/caching.md', <<~MARKDOWN
157 # Caching Strategies
158
159 ## Fragment Caching (Views)
160
161 Basic fragment cache:
162 ```erb
163 <% cache @user do %>
164 <%= @user.name %>
165 <% end %>
166 ```
167
168 ## Russian Doll Caching
169
170 Nested caches that auto-expire:
171 ```erb
172 <% cache @project do %>
173 <h1><%= @project.name %></h1>
174 <%= render @project.tasks %>
175 <% end %>
176
177 <!-- _task.html.erb -->
178 <% cache task do %>
179 <%= task.name %>
180 <% end %>
181 ```
182
183 **Important:** Use `touch: true` on associations:
184 ```ruby
185 class Task < ApplicationRecord
186 belongs_to :project, touch: true
187 end
188 ```
189
190 ## Model Caching (IdentityCache)
191
192 ```ruby
193 class User < ApplicationRecord
194 include Cacheable
195
196 cache_index :email
197 cache_has_many :posts, embed: true
198 end
199
200 # Usage
201 User.fetch(1) # Cached
202 User.fetch_by_email('a@b.com') # Cached
203 user.fetch_posts # Cached with user
204 ```
205
206 ## Low-Level Caching
207
208 ```ruby
209 Rails.cache.fetch('expensive_operation', expires_in: 1.hour) do
210 ExpensiveService.call
211 end
212 ```
213
214 ## Cache Warming
215
216 ```ruby
217 # Run periodically
218 CacheWarmerJob.perform_later
219
220 # Or specific caches
221 CacheWarmerJob.perform_later(caches: ['users'])
222 ```
223MARKDOWN
224
225say 'Caching strategies configured!', :green
226say ' Use `include Cacheable` in models for identity caching', :cyan
227say ' See docs/caching.md for patterns', :cyan