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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Isolating expensive aggregate reads on a replica protects the primary database from analytical load.
  2. 2Wrapping queries in a single on_replica helper keeps role-switching out of each individual method.
  3. 3while_preventing_writes turns replica routing into a safety guarantee, not just a hint.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Routing heavy reports to a read replica in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code