ruby
52 lines · 6 steps
Routing heavy reports to a read replica in Rails
A query object and a controller concern push analytical reads onto the reading replica to keep the primary free for writes.
Explained by
highlit
1class ReportingQuery
2 def initialize(scope = ApplicationRecord)
3 @scope = scope
4 end
5
6 def on_replica(&block)
7 @scope.connected_to(role: :reading) do
8 block.call
9 end
10 end
11
12 def monthly_revenue(store_id:, month:)
13 on_replica do
14 Order
15 .where(store_id: store_id)
16 .where(placed_at: month.all_month)
17 .where(status: :fulfilled)
18 .group("DATE_TRUNC('day', placed_at)")
19 .sum(:total_cents)
20 end
21 end
22
23 def top_customers(limit: 25)
24 on_replica do
25 Customer
26 .joins(:orders)
27 .select("customers.*, SUM(orders.total_cents) AS lifetime_cents")
28 .group("customers.id")
29 .order("lifetime_cents DESC")
30 .limit(limit)
31 .to_a
32 end
33 end
34end
35
36module ReplicaReporting
37 extend ActiveSupport::Concern
38
39 included do
40 around_action :route_reports_to_replica, only: %i[index show export]
41 end
42
43 private
44
45 def route_reports_to_replica(&block)
46 ActiveRecord::Base.connected_to(role: :reading) do
47 ActiveRecord::Base.while_preventing_writes do
48 block.call
49 end
50 end
51 end
52end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Isolating expensive aggregate reads on a replica protects the primary database from analytical load.
- 2Wrapping queries in a single on_replica helper keeps role-switching out of each individual method.
- 3while_preventing_writes turns replica routing into a safety guarantee, not just a hint.
Related explainers
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/routing-heavy-reports-to-a-read-replica-in-rails-explained-ruby-c14b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.