GitHunt
JA

jaydorsey/active_record_stuff

Stuff about active record

README

What is this?

It's a Rails 8 repo with some ActiveRecord examples, based on notes I took when reading
Advanced Active Record

Prerequisites

  1. Docker

Setup

  1. Clone this repo
  2. docker compose run web bin/setup --skip-server --reset

has_one

These are ActiveRecord examples with a simple has_one relationship

Raises an error, because one of the accounts is missing a supplier (this was intentional)

Account.where("account_number ilike '%123%'").map { |account| account.supplier.name }
  Account Load (0.6ms)  SELECT "accounts".* FROM "accounts" WHERE (account_number ilike '%123%')
  Supplier Load (0.3ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  Supplier Load (0.1ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
  Supplier Load (0.1ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
(active-record-stuff):7:in 'block in <main>': undefined method 'name' for nil (NoMethodError)
        from (active-record-stuff):7:in 'Enumerable#map'
        from (active-record-stuff):7:in '<main>'

This fixes that, but for that error (missing data) it creates an N+1

Account.joins(:supplier).where("account_number ilike '%123%'").map { |account| account.supplier.name }
  Account Load (1.7ms)  SELECT "accounts".* FROM "accounts" INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id" WHERE (account_number ilike '%123%')
  Supplier Load (0.6ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  Supplier Load (0.2ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
  Supplier Load (0.1ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]

This fixes the N+1, but takes us back to the nil supplier error

Account.includes(:supplier).where("account_number ilike '%123%'").map { |account| account.supplier.name }
  Account Load (0.6ms)  SELECT "accounts".* FROM "accounts" WHERE (account_number ilike '%123%')
  Supplier Load (0.4ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
(active-record-stuff):4:in 'block in <main>': undefined method 'name' for nil (NoMethodError)
        from (active-record-stuff):4:in 'Enumerable#map'
        from (active-record-stuff):4:in '<main>'

Using joins + includes takes care of both, and you get a single query.

Note that active record chooses to do an eager load. The docs
explain it

Account.joins(:supplier).includes(:supplier).where("account_number ilike '%123%'").map { |account| account.supplier.name }
  Account Eager Load (2.7ms)  SELECT "accounts"."id" AS t0_r0, "accounts"."account_number" AS t0_r1, "accounts"."created_at" AS t0_r2, "accounts"."supplier_id" AS t0_r3, "accounts"."updated_at" AS t0_r4, "suppliers"."id" AS t1_r0, "suppliers"."created_at" AS t1_r1, "suppliers"."name" AS t1_r2, "suppliers"."updated_at" AS t1_r3 FROM "accounts" INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id" WHERE (account_number ilike '%123%')

You can also force the eager load so it does 1 query

Account.joins(:supplier).eager_load(:supplier).where("account_number ilike '%123%'").map { |account| account.supplier.name }
  Account Eager Load (0.7ms)  SELECT "accounts"."id" AS t0_r0, "accounts"."account_number" AS t0_r1, "accounts"."created_at" AS t0_r2, "accounts"."supplier_id" AS t0_r3, "accounts"."updated_at" AS t0_r4, "suppliers"."id" AS t1_r0, "suppliers"."created_at" AS t1_r1, "suppliers"."name" AS t1_r2, "suppliers"."updated_at" AS t1_r3 FROM "accounts" INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id" WHERE (account_number ilike '%123%')

Or force the preload so it does 2 queries (1 each for account + suppliers)

Account.joins(:supplier).preload(:supplier).where("account_number ilike '%123%'").map { |account| account.supplier.name }
  Account Load (0.7ms)  SELECT "accounts".* FROM "accounts" INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id" WHERE (account_number ilike '%123%')
  Supplier Load (0.4ms)  SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]

When your where clause doesn't reference the includes table, ActiveRecord chooses a preload

Supplier.includes(:account).where("name ilike '%Acme%'")
  Supplier Load (0.5ms)  SELECT "suppliers".* FROM "suppliers" WHERE (name ilike '%Acme%') /* loading for pp */ LIMIT $1  [["LIMIT", 11]]
  Account Load (0.5ms)  SELECT "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = $1  [["supplier_id", 1]]

If you want to reference the included table from your where clause, ActiveRecord will (can?) choose an eager_load and
force a left_join

Supplier.includes(:account).references(:account).where("accounts.account_number ilike '%123%'")
  Supplier Eager Load (2.6ms)  SELECT "suppliers"."id" AS t0_r0, "suppliers"."created_at" AS t0_r1, "suppliers"."name" AS t0_r2, "suppliers"."updated_at" AS t0_r3, "accounts"."id" AS t1_r0, "accounts"."account_number" AS t1_r1, "accounts"."created_at" AS t1_r2, "accounts"."supplier_id" AS t1_r3, "accounts"."updated_at" AS t1_r4 FROM "suppliers" LEFT OUTER JOIN "accounts" ON "accounts"."supplier_id" = "suppliers"."id" WHERE (accounts.account_number ilike '%123%') /* loading for pp */ LIMIT $1  [["LIMIT", 11]]

Use a joins to force a join, ignoring any records missing a related record

Account.joins(:supplier).where("account_number ilike '%123%'")
  Account Load (0.9ms)  SELECT "accounts".* FROM "accounts" INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id" WHERE (account_number ilike '%123%') /* loading for pp */ LIMIT $1  [["LIMIT", 11]]

Using joins plus eager_load to query across

Account.joins(:supplier).eager_load(:supplier).where("suppliers.name ilike '%Acme%'")
  Account Eager Load (1.0ms)  SELECT "accounts"."id" AS t0_r0, "accounts"."account_number" AS t0_r1, "accounts"."created_at" AS t0_r2, "accounts"."supplier_id" AS t0_r3, "accounts"."updated_at" AS t0_r4, "suppliers"."id" AS t1_r0, "suppliers"."created_at" AS t1_r1, "suppliers"."name" AS t1_r2, "suppliers"."updated_at" AS t1_r3 FROM "accounts" INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id" WHERE (suppliers.name ilike '%Acme%') /* loading for pp */ LIMIT $1  [["LIMIT", 11]]

joins vs. includes

One final example, a comparison between a joins:

Account.joins(:supplier).eager_load(:supplier).where("suppliers.name ~ '[aeiou]'")
SELECT
	"accounts"."id" AS t0_r0,
	"accounts"."account_number" AS t0_r1,
	"accounts"."created_at" AS t0_r2,
	"accounts"."supplier_id" AS t0_r3,
	"accounts"."updated_at" AS t0_r4,
	"suppliers"."id" AS t1_r0,
	"suppliers"."created_at" AS t1_r1,
	"suppliers"."name" AS t1_r2,
	"suppliers"."updated_at" AS t1_r3
FROM
	"accounts"
	INNER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id"
WHERE
	(suppliers.name ~ '[aeiou]')

And an includes:

Account.includes(:supplier).eager_load(:supplier).where("suppliers.name ~ '[aeiou]'")
SELECT
	"accounts"."id" AS t0_r0,
	"accounts"."account_number" AS t0_r1,
	"accounts"."created_at" AS t0_r2,
	"accounts"."supplier_id" AS t0_r3,
	"accounts"."updated_at" AS t0_r4,
	"suppliers"."id" AS t1_r0,
	"suppliers"."created_at" AS t1_r1,
	"suppliers"."name" AS t1_r2,
	"suppliers"."updated_at" AS t1_r3
FROM
	"accounts"
	LEFT OUTER JOIN "suppliers" ON "suppliers"."id" = "accounts"."supplier_id"
WHERE
	(suppliers.name ~ '[aeiou]')

The primary difference is:

  1. joins uses an INNER JOIN
  2. includes uses a LEFT OUTER JOIN

The outcome is the same, but the performance likely won't be for certain scenarios. The joins is likely to be the more
efficient version (comparing query plans, at least)

Languages

HTML45.7%Ruby45.5%Dockerfile3.4%Shell2.8%JavaScript2.0%CSS0.6%

Contributors

Created January 21, 2026
Updated January 21, 2026