Skip to content

updateOrCreate

Find the first row matching attributes. If found, update it with values. If not, INSERT { ...attributes, ...values }. This is the user-space upsert.

Signature

ts
FedacoBuilder<T>.updateOrCreate(
  attributes: Record<string, any>,
  values: Record<string, any>,
): Promise<T>

Parameters

NameRequiredDescription
attributesUsed in the WHERE clause and as the lookup key. Always written to the row.
valuesUpdated onto the matched row, or merged into the INSERT for the create path.

Returns

A Promise<T> resolving to the persisted model — either the updated existing row or the freshly inserted one. Check _wasRecentlyCreated to distinguish:

ts
const user = await User.createQuery().updateOrCreate({ email: 'a@x.com' }, { name: 'A' });
if (user._wasRecentlyCreated) {
  // INSERT path
} else {
  // UPDATE path
}

Real-World Use Cases

1. Idempotent webhook handler

ts
async function syncFromStripeEvent(event: StripeEvent) {
  await Customer.createQuery().updateOrCreate(
    { stripe_id: event.customer.id },
    {
      email: event.customer.email,
      plan: event.customer.subscription?.plan ?? 'free',
      synced_at: new Date(),
    },
  );
}

Receiving the same webhook twice produces the same final row — second call updates instead of erroring.

2. Daily metrics roll-up

ts
await DailyRevenue.createQuery().updateOrCreate(
  { day: today },
  { cents: totalCentsForDay },
);

Schedule it nightly without worrying whether the row already exists.

3. Cross-connection upsert

ts
await User.useConnection('second').updateOrCreate(
  { email: 'admin@example.com' },
  { role: 'admin', active: true },
);

4. Inside a transaction

ts
await db().transaction(async (tx) => {
  const tag = await Tag.createQuery(tx).updateOrCreate(
    { slug },
    { name, last_used_at: new Date() },
  );
  await Post.createQuery(tx).create({ primary_tag_id: tag.id, ... });
});

5. Bulk version with limited concurrency

updateOrCreate is per-row. For many records, batch them carefully — concurrent calls on the same key race:

ts
for (const item of records) {
  await Stat.createQuery().updateOrCreate({ key: item.key }, { value: item.value });
}

For real bulk upsert without N round trips, see upsert on the query builder, which compiles to driver-native INSERT ... ON CONFLICT / ON DUPLICATE KEY UPDATE.

updateOrCreate vs firstOrCreate vs upsert

ToolLookup hit →Lookup miss →Round trips
firstOrCreatereturn as-isINSERT2 (SELECT + maybe INSERT)
updateOrCreateUPDATEINSERT2-3 (SELECT + UPDATE/INSERT)
upsert (builder)Driver-native UPDATEDriver-native INSERT1

For a few rows on a critical path, updateOrCreate is fine. For thousands, switch to upsert.

Common Pitfalls

  • Race conditions — two concurrent calls can both miss the lookup and both insert. Add a unique index on the attributes columns; on duplicate-key error, retry as an UPDATE.
  • Make attributes minimal and unique. They're the lookup key. Adding non-key columns there means you'll never find existing rows.
  • values overrides attributes during INSERT. If you want a column to be set only on insert (immutable lookup key), put it in attributes only.

See Also

  • firstOrCreate — insert if missing, never update.
  • firstOrNew — return without persisting.
  • save — manual save after instance mutations.
  • upsert — driver-native bulk upsert.

Released under the MIT License.