rails_claude_skills 0.1.0
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 +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
- data/.github/ISSUE_TEMPLATE/question.yml +90 -0
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/release.yml +66 -0
- data/.rubocop.yml +52 -0
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +332 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +580 -0
- data/LICENSE.txt +21 -0
- data/README.md +544 -0
- data/Rakefile +8 -0
- data/lib/generators/claude/agent/agent_generator.rb +71 -0
- data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
- data/lib/generators/claude/command/command_generator.rb +50 -0
- data/lib/generators/claude/command/templates/command.md.tt +28 -0
- data/lib/generators/claude/commands_library/create-pr.md +27 -0
- data/lib/generators/claude/commands_library/dbchange.md +19 -0
- data/lib/generators/claude/commands_library/quality.md +20 -0
- data/lib/generators/claude/commands_library/stimulus.md +19 -0
- data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
- data/lib/generators/claude/install/install_generator.rb +211 -0
- data/lib/generators/claude/install/templates/README.md.tt +59 -0
- data/lib/generators/claude/install/templates/USAGE +28 -0
- data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
- data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
- data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
- data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
- data/lib/generators/claude/rule/rule_generator.rb +175 -0
- data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
- data/lib/generators/claude/rules_library/code-style.md +37 -0
- data/lib/generators/claude/rules_library/database.md +47 -0
- data/lib/generators/claude/rules_library/hotwire.md +56 -0
- data/lib/generators/claude/rules_library/security.md +54 -0
- data/lib/generators/claude/rules_library/testing.md +47 -0
- data/lib/generators/claude/skill/skill_generator.rb +196 -0
- data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
- data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
- data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
- data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
- data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
- data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
- data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
- data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
- data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
- data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
- data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
- data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
- data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
- data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
- data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
- data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
- data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
- data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
- data/lib/generators/claude/views/views_generator.rb +113 -0
- data/lib/rails_claude_skills/railtie.rb +16 -0
- data/lib/rails_claude_skills/version.rb +5 -0
- data/lib/rails_claude_skills.rb +27 -0
- data/sig/rails_claude_skills.rbs +4 -0
- metadata +199 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# Performance Optimization for Kaminari Pagination
|
|
2
|
+
|
|
3
|
+
Strategies and best practices for optimizing pagination performance in Rails applications.
|
|
4
|
+
|
|
5
|
+
## The COUNT Query Problem
|
|
6
|
+
|
|
7
|
+
By default, Kaminari executes two queries:
|
|
8
|
+
1. The main SELECT query to fetch records
|
|
9
|
+
2. A COUNT query to get total records
|
|
10
|
+
|
|
11
|
+
For large tables, the COUNT query can be expensive.
|
|
12
|
+
|
|
13
|
+
```sql
|
|
14
|
+
-- Main query
|
|
15
|
+
SELECT * FROM posts ORDER BY created_at LIMIT 25 OFFSET 0;
|
|
16
|
+
|
|
17
|
+
-- Count query (expensive on large tables)
|
|
18
|
+
SELECT COUNT(*) FROM posts;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Skip COUNT with without_count
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Controller
|
|
25
|
+
def index
|
|
26
|
+
@posts = Post.order(:created_at).page(params[:page]).without_count
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# View - limited helpers available
|
|
30
|
+
<%= link_to_prev_page @posts, 'Previous' %>
|
|
31
|
+
<span>Page <%= @posts.current_page %></span>
|
|
32
|
+
<%= link_to_next_page @posts, 'Next' %>
|
|
33
|
+
|
|
34
|
+
# These won't work with without_count:
|
|
35
|
+
# - total_pages
|
|
36
|
+
# - total_count
|
|
37
|
+
# - numbered page links
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Conditional COUNT Query
|
|
41
|
+
|
|
42
|
+
Only run COUNT on first page or when explicitly requested:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# app/controllers/posts_controller.rb
|
|
46
|
+
def index
|
|
47
|
+
@posts = Post.order(:created_at).page(params[:page])
|
|
48
|
+
|
|
49
|
+
# Skip count after first page
|
|
50
|
+
@posts = @posts.without_count unless first_page?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def first_page?
|
|
56
|
+
params[:page].blank? || params[:page].to_i <= 1
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Caching Total Count
|
|
61
|
+
|
|
62
|
+
Cache the count query result:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# app/models/post.rb
|
|
66
|
+
class Post < ApplicationRecord
|
|
67
|
+
def self.cached_total_count
|
|
68
|
+
Rails.cache.fetch(['Post', 'total_count'], expires_in: 5.minutes) do
|
|
69
|
+
count
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# app/controllers/posts_controller.rb
|
|
75
|
+
def index
|
|
76
|
+
@posts = Post.order(:created_at).page(params[:page]).without_count
|
|
77
|
+
@total_count = Post.cached_total_count
|
|
78
|
+
@total_pages = (@total_count.to_f / Post.default_per_page).ceil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# View
|
|
82
|
+
<div class="pagination-info">
|
|
83
|
+
Page <%= @posts.current_page %> of approximately <%= @total_pages %>
|
|
84
|
+
</div>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Counter Cache for Associations
|
|
88
|
+
|
|
89
|
+
Use counter caches to avoid counting associated records:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Migration
|
|
93
|
+
class AddPostsCountToCategories < ActiveRecord::Migration[7.0]
|
|
94
|
+
def change
|
|
95
|
+
add_column :categories, :posts_count, :integer, default: 0, null: false
|
|
96
|
+
|
|
97
|
+
# Populate existing counts
|
|
98
|
+
reversible do |dir|
|
|
99
|
+
dir.up do
|
|
100
|
+
Category.find_each do |category|
|
|
101
|
+
Category.reset_counters(category.id, :posts)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Model
|
|
109
|
+
class Post < ApplicationRecord
|
|
110
|
+
belongs_to :category, counter_cache: true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Controller - use counter cache instead of count
|
|
114
|
+
def index
|
|
115
|
+
@category = Category.find(params[:category_id])
|
|
116
|
+
@posts = @category.posts.page(params[:page])
|
|
117
|
+
@total_count = @category.posts_count # Fast! No COUNT query
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Database Indexes
|
|
122
|
+
|
|
123
|
+
Add indexes for paginated queries:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Migration
|
|
127
|
+
class AddIndexesToPosts < ActiveRecord::Migration[7.0]
|
|
128
|
+
def change
|
|
129
|
+
# For ORDER BY created_at
|
|
130
|
+
add_index :posts, :created_at
|
|
131
|
+
|
|
132
|
+
# For filtered pagination
|
|
133
|
+
add_index :posts, [:category_id, :created_at]
|
|
134
|
+
add_index :posts, [:published, :created_at]
|
|
135
|
+
|
|
136
|
+
# Composite index for common queries
|
|
137
|
+
add_index :posts, [:user_id, :status, :created_at]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Eager Loading (N+1 Prevention)
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# Bad - N+1 queries
|
|
146
|
+
def index
|
|
147
|
+
@posts = Post.page(params[:page])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# View triggers N+1
|
|
151
|
+
<% @posts.each do |post| %>
|
|
152
|
+
<%= post.user.name %> # Query for each post!
|
|
153
|
+
<%= post.comments.count %> # Query for each post!
|
|
154
|
+
<% end %>
|
|
155
|
+
|
|
156
|
+
# Good - eager loading
|
|
157
|
+
def index
|
|
158
|
+
@posts = Post.includes(:user, :comments)
|
|
159
|
+
.page(params[:page])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Multiple associations
|
|
163
|
+
@posts = Post.includes(:user, :category, comments: :user)
|
|
164
|
+
.page(params[:page])
|
|
165
|
+
|
|
166
|
+
# Nested associations
|
|
167
|
+
@posts = Post.includes(comments: [:user, :likes])
|
|
168
|
+
.page(params[:page])
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Select Only Needed Columns
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Bad - loads all columns
|
|
175
|
+
@posts = Post.page(params[:page])
|
|
176
|
+
|
|
177
|
+
# Good - only load needed columns
|
|
178
|
+
@posts = Post.select(:id, :title, :created_at, :user_id)
|
|
179
|
+
.page(params[:page])
|
|
180
|
+
|
|
181
|
+
# Even better with pluck for simple lists
|
|
182
|
+
@post_titles = Post.order(:created_at)
|
|
183
|
+
.page(params[:page])
|
|
184
|
+
.pluck(:id, :title)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Pagination with Scopes
|
|
188
|
+
|
|
189
|
+
Use scopes to avoid repeated query building:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# app/models/post.rb
|
|
193
|
+
class Post < ApplicationRecord
|
|
194
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
195
|
+
scope :published, -> { where(published: true) }
|
|
196
|
+
scope :by_category, ->(category_id) { where(category_id: category_id) if category_id.present? }
|
|
197
|
+
scope :paginated, ->(page, per_page = 25) { page(page).per(per_page) }
|
|
198
|
+
|
|
199
|
+
# Optimized scope with eager loading
|
|
200
|
+
scope :with_associations, -> { includes(:user, :category) }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Controller
|
|
204
|
+
def index
|
|
205
|
+
@posts = Post.published
|
|
206
|
+
.by_category(params[:category_id])
|
|
207
|
+
.with_associations
|
|
208
|
+
.recent
|
|
209
|
+
.paginated(params[:page], 20)
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Cursor-Based Pagination
|
|
214
|
+
|
|
215
|
+
For large datasets or real-time feeds, use cursor-based pagination:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# app/controllers/posts_controller.rb
|
|
219
|
+
def index
|
|
220
|
+
@posts = fetch_posts_by_cursor
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def fetch_posts_by_cursor
|
|
226
|
+
posts = Post.order(id: :desc)
|
|
227
|
+
|
|
228
|
+
if params[:after_id]
|
|
229
|
+
posts = posts.where('id < ?', params[:after_id])
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
posts.limit(20)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# View
|
|
236
|
+
<% if @posts.any? %>
|
|
237
|
+
<%= render @posts %>
|
|
238
|
+
<%= link_to 'Load More', posts_path(after_id: @posts.last.id),
|
|
239
|
+
class: 'load-more' %>
|
|
240
|
+
<% end %>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Benefits of cursor pagination:
|
|
244
|
+
- No COUNT query needed
|
|
245
|
+
- Consistent results even when data changes
|
|
246
|
+
- Better performance on large offsets
|
|
247
|
+
- Works well with infinite scroll
|
|
248
|
+
|
|
249
|
+
## Fragment Caching
|
|
250
|
+
|
|
251
|
+
Cache rendered pagination:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# View
|
|
255
|
+
<% cache ["posts-index", @posts.current_page, @posts.updated_at.to_i] do %>
|
|
256
|
+
<%= render @posts %>
|
|
257
|
+
<% end %>
|
|
258
|
+
|
|
259
|
+
<%= paginate @posts %>
|
|
260
|
+
|
|
261
|
+
# Russian Doll Caching
|
|
262
|
+
<% cache ["posts-index", @posts.current_page] do %>
|
|
263
|
+
<% @posts.each do |post| %>
|
|
264
|
+
<% cache post do %>
|
|
265
|
+
<%= render post %>
|
|
266
|
+
<% end %>
|
|
267
|
+
<% end %>
|
|
268
|
+
<% end %>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Query Result Caching
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# app/controllers/posts_controller.rb
|
|
275
|
+
def index
|
|
276
|
+
cache_key = ['posts-page', params[:page], params[:category_id]].compact.join('-')
|
|
277
|
+
|
|
278
|
+
@posts = Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
|
279
|
+
Post.published
|
|
280
|
+
.by_category(params[:category_id])
|
|
281
|
+
.includes(:user)
|
|
282
|
+
.page(params[:page])
|
|
283
|
+
.to_a # Convert to array to cache results
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Approximate COUNT for Large Tables
|
|
289
|
+
|
|
290
|
+
For very large tables, use approximate counts:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# PostgreSQL
|
|
294
|
+
def approximate_count
|
|
295
|
+
result = ActiveRecord::Base.connection.execute(
|
|
296
|
+
"SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = 'posts'"
|
|
297
|
+
)
|
|
298
|
+
result[0]['estimate'].to_i
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Use in controller
|
|
302
|
+
def index
|
|
303
|
+
@posts = Post.page(params[:page]).without_count
|
|
304
|
+
@approximate_total = Post.approximate_count
|
|
305
|
+
@approximate_pages = (@approximate_total.to_f / Post.default_per_page).ceil
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Partial Pagination
|
|
310
|
+
|
|
311
|
+
Only paginate when necessary:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# app/controllers/posts_controller.rb
|
|
315
|
+
def index
|
|
316
|
+
@posts = Post.order(:created_at)
|
|
317
|
+
|
|
318
|
+
# Only paginate if there are many records
|
|
319
|
+
if @posts.count > 50
|
|
320
|
+
@posts = @posts.page(params[:page])
|
|
321
|
+
@use_pagination = true
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# View
|
|
326
|
+
<%= render @posts %>
|
|
327
|
+
<%= paginate @posts if @use_pagination %>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Database-Specific Optimizations
|
|
331
|
+
|
|
332
|
+
### PostgreSQL
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
# Use DISTINCT ON for unique pagination
|
|
336
|
+
@posts = Post.select('DISTINCT ON (posts.user_id) posts.*')
|
|
337
|
+
.order('posts.user_id, posts.created_at DESC')
|
|
338
|
+
.page(params[:page])
|
|
339
|
+
|
|
340
|
+
# Use materialized views for complex queries
|
|
341
|
+
# db/migrate/xxx_create_popular_posts_view.rb
|
|
342
|
+
execute <<-SQL
|
|
343
|
+
CREATE MATERIALIZED VIEW popular_posts AS
|
|
344
|
+
SELECT posts.*, COUNT(likes.id) as likes_count
|
|
345
|
+
FROM posts
|
|
346
|
+
LEFT JOIN likes ON likes.post_id = posts.id
|
|
347
|
+
GROUP BY posts.id
|
|
348
|
+
ORDER BY likes_count DESC
|
|
349
|
+
SQL
|
|
350
|
+
|
|
351
|
+
# Refresh materialized view periodically
|
|
352
|
+
PopularPost.connection.execute('REFRESH MATERIALIZED VIEW popular_posts')
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### MySQL
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
# Use SQL_CALC_FOUND_ROWS (MySQL only)
|
|
359
|
+
class Post < ApplicationRecord
|
|
360
|
+
def self.paginate_with_total(page, per_page = 25)
|
|
361
|
+
offset = (page - 1) * per_page
|
|
362
|
+
|
|
363
|
+
posts = connection.select_all(
|
|
364
|
+
"SELECT SQL_CALC_FOUND_ROWS * FROM posts LIMIT #{per_page} OFFSET #{offset}"
|
|
365
|
+
).to_a
|
|
366
|
+
|
|
367
|
+
total = connection.select_value('SELECT FOUND_ROWS()')
|
|
368
|
+
|
|
369
|
+
Kaminari.paginate_array(posts, total_count: total).page(page).per(per_page)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Monitoring Performance
|
|
375
|
+
|
|
376
|
+
### Query Analysis
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
# development.rb
|
|
380
|
+
config.after_initialize do
|
|
381
|
+
ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, details|
|
|
382
|
+
duration = ((finish - start) * 1000).round(2)
|
|
383
|
+
if duration > 100 # Log slow queries
|
|
384
|
+
Rails.logger.warn "SLOW QUERY (#{duration}ms): #{details[:sql]}"
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Bullet Gem
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
# Gemfile
|
|
394
|
+
group :development do
|
|
395
|
+
gem 'bullet'
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# config/environments/development.rb
|
|
399
|
+
config.after_initialize do
|
|
400
|
+
Bullet.enable = true
|
|
401
|
+
Bullet.alert = true
|
|
402
|
+
Bullet.bullet_logger = true
|
|
403
|
+
Bullet.console = true
|
|
404
|
+
Bullet.rails_logger = true
|
|
405
|
+
end
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Benchmark Pagination
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# Controller
|
|
412
|
+
def index
|
|
413
|
+
time = Benchmark.measure do
|
|
414
|
+
@posts = Post.includes(:user, :category)
|
|
415
|
+
.order(:created_at)
|
|
416
|
+
.page(params[:page])
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
Rails.logger.info "Pagination took: #{time.real} seconds"
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Best Practices Summary
|
|
424
|
+
|
|
425
|
+
1. **Use indexes**: Add indexes for ORDER BY and WHERE clauses
|
|
426
|
+
2. **Eager load associations**: Use `includes` to prevent N+1 queries
|
|
427
|
+
3. **Skip COUNT when possible**: Use `without_count` for large datasets
|
|
428
|
+
4. **Cache counts**: Cache expensive COUNT queries
|
|
429
|
+
5. **Use counter caches**: For association counts
|
|
430
|
+
6. **Select only needed columns**: Reduce data transfer
|
|
431
|
+
7. **Consider cursor pagination**: For real-time feeds and large datasets
|
|
432
|
+
8. **Fragment cache**: Cache rendered pagination results
|
|
433
|
+
9. **Monitor query performance**: Use Bullet and query logs
|
|
434
|
+
10. **Test with production data**: Pagination performance issues often only appear at scale
|
|
435
|
+
|
|
436
|
+
## Performance Checklist
|
|
437
|
+
|
|
438
|
+
Before deploying pagination:
|
|
439
|
+
|
|
440
|
+
- [ ] Added indexes for ORDER BY columns
|
|
441
|
+
- [ ] Eager loaded all associations used in views
|
|
442
|
+
- [ ] Considered using `without_count` for large tables
|
|
443
|
+
- [ ] Tested with production-sized dataset
|
|
444
|
+
- [ ] Verified no N+1 queries with Bullet
|
|
445
|
+
- [ ] Measured query execution time
|
|
446
|
+
- [ ] Implemented caching strategy if needed
|
|
447
|
+
- [ ] Set reasonable `max_per_page` limit
|
|
448
|
+
- [ ] Tested pagination edge cases (first, last, empty)
|
|
449
|
+
- [ ] Verified mobile/responsive performance
|
|
450
|
+
|
|
451
|
+
## Measuring Impact
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
# Before optimization
|
|
455
|
+
User.page(10).per(25)
|
|
456
|
+
# => 2 queries: SELECT (0.5ms), COUNT (150ms) - SLOW!
|
|
457
|
+
|
|
458
|
+
# After adding index
|
|
459
|
+
add_index :users, :created_at
|
|
460
|
+
|
|
461
|
+
User.order(:created_at).page(10).per(25)
|
|
462
|
+
# => 2 queries: SELECT (0.5ms), COUNT (0.8ms) - FAST!
|
|
463
|
+
|
|
464
|
+
# After using without_count
|
|
465
|
+
User.order(:created_at).page(10).per(25).without_count
|
|
466
|
+
# => 1 query: SELECT (0.5ms) - FASTEST!
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## When to Optimize
|
|
470
|
+
|
|
471
|
+
Optimize pagination when:
|
|
472
|
+
- Tables have > 10,000 records
|
|
473
|
+
- COUNT queries take > 100ms
|
|
474
|
+
- Users report slow page loads
|
|
475
|
+
- Database CPU usage is high
|
|
476
|
+
- You're hitting query timeouts
|
|
477
|
+
|
|
478
|
+
Start simple, measure, then optimize based on real performance data.
|