๐Ÿ”€

find_or_initialize_by vs create_or_find_by

Why RecordNotUnique happens โ€” the order of find and create

Press the same button in two tabs at nearly the same time and ActiveRecord::RecordNotUnique blows up. If that error shows up in your error tracker, the culprit is almost always find_or_initialize_by or find_or_create_by.

Why it blows up

find_or_initialize_by runs a SELECT first, and if nothing is found, it only calls new in memory. The INSERT happens later, when you call save. The problem is the gap between finding and saving.

Request A runs the SELECT. Nothing there. Request B runs the SELECT too โ€” A has not INSERTed yet, so B also sees nothing. A INSERTs successfully. B INSERTs and dies on the unique index.

find_or_create_by has the exact same find โ†’ create order, so it cannot close this gap either. Adding a bang does not change the order.

create_or_find_by flips the order

No SELECT โ€” it just tries the INSERT. If the row already exists and the unique constraint fires, it rescues that RecordNotUnique internally and SELECTs the existing record instead.

The existence check moves from your application SELECT to the database unique constraint. Under concurrent requests, one side wins the INSERT and the other gets the existing record. No error escapes.

One precondition

The lookup columns need a unique index. Without one there is no conflict to catch, and duplicate rows pile up silently. create_or_find_by without a unique index is just create.

The rule of thumb is simple: need an unsaved object to render a form? find_or_initialize_by. Need to safely grab a record that concurrent requests might create? create_or_find_by.

The three methods compared

Aspect find_or_initialize_by find_or_create_by create_or_find_by
OrderSELECT โ†’ newSELECT โ†’ INSERTINSERT โ†’ SELECT on conflict
Returned stateunsaved new_record if missingpersistedpersisted
Under concurrencyRecordNotUnique at saveRecordNotUnique at createreturns existing record, no error
Preconditionโ€”โ€”unique index required

Race condition timeline

Tab A: SELECT โ†’ nothing
Tab B: SELECT โ†’ nothing (A has not INSERTed yet, so B sees nothing)
Tab A: INSERT โ†’ success
Tab B: INSERT โ†’ unique index violation โ†’ RecordNotUnique

What create_or_find_by looks like inside

def create_or_find_by(attrs)
  transaction(requires_new: true) { create(attrs) }
rescue ActiveRecord::RecordNotUnique
  find_by!(attrs)
end

requires_new: true creates a savepoint, so a failed INSERT does not poison an outer transaction. This matters a lot on PostgreSQL.

โš  Pointless without a unique index

No conflict means no rescue. Put a unique index on the lookup columns first. Also remember it INSERTs immediately with only the attributes you pass โ€” if other columns have required validations, it fails right there.

Key Points

1

find_or_initialize_by: SELECT, then new if missing โ€” the INSERT runs at save time

2

Concurrent requests both see nothing and both INSERT โ€” the slower one dies with RecordNotUnique

3

find_or_create_by(!) has the same find โ†’ create order, so it fails the same way

4

create_or_find_by INSERTs first โ€” on unique violation it rescues and SELECTs the existing record

5

Precondition: a unique index on the lookup columns โ€” without it duplicates pile up silently

6

It uses a transaction(requires_new: true) savepoint, so it is safe inside an outer transaction

Pros

  • Existence check is delegated to the DB unique constraint, so no error escapes under concurrency
  • The returned record is always persisted
  • Uses a savepoint (requires_new: true), so it is safe inside an outer transaction

Cons

  • Without a unique index it is just create โ€” duplicates pile up silently
  • INSERTs immediately with only the given attributes, so other required-column validations can fail
  • Rare case: if the winning transaction rolls back right after the conflict, find_by! finds nothing

Use Cases

Upserting one-per-user records like settings or saved filters Toggle-style records like likes and bookmarks where duplicates are forbidden Auto-creating tags or master data keyed by name Idempotent webhook handlers that may receive the same event twice