MasterRecord

Best Practices

Hard-won patterns for production apps.

MasterRecord is flexible, but a few conventions keep your code fast, correct, and easy to reason about.

1. Scope the context per request — never a singleton#

A context is a unit of work, exactly like an EF Core DbContext. Register it per request (master.addScoped('db', AppContext)this.db in controllers), build a scope per job run (master.useScope() / withDb()), or new AppContext() + close() around each unit of work. A module-level context shared across concurrent requests mixes change sets, grows memory without bound and was the root of the lost-write class of bugs hardened in 1.5.9–1.5.12.

javascript
master.addScoped('db', AppContext);            // ✅ fresh context per request, disposed on response close
await master.useScope((s) => s.db.User.toList()); // ✅ jobs / scripts
// export default new AppContext();             ❌ process-wide singleton shared by every request

See Change Tracking & Context Lifetime.

2. Always await writes#

Every engine is asynchronous. Await every save() and saveChanges() so errors surface and ordering is guaranteed.

javascript
await user.save();          // ✅
await db.saveChanges();     // ✅
user.save();                // ❌ unhandled promise, lost errors

3. asNoTracking() for reads#

Queries track their results by default. For listings and read-only endpoints use asNoTracking() (EF AsNoTracking()) — nothing is retained, and a read-heavy path never grows the tracked set. Project with pluck() / select() / toObjectList() when you only need columns.

javascript
const feed = await db.Post.asNoTracking().orderByDescending((p) => p.id).take(50).toList();

Hot loops: when you correlate two result sets, build a one-pass Map keyed by the join column instead of a nested loop — an O(n²) scan over two tables of a few thousand rows is millions of property reads. Since 1.23.0 an entity property read costs about what a plain-object read costs (~20 ns; it was ~300 ns before, because every row carried its own prototype), but the algorithmic fix still wins by orders of magnitude.

one-pass correlation
const tenants = await db.Tenant.asNoTracking().toList();
const usage   = await db.Usage.asNoTracking().toList();
const byTenant = new Map();                                  // one pass over usage
for (const u of usage) (byTenant.get(u.tenantId) ?? byTenant.set(u.tenantId, []).get(u.tenantId)).push(u);
const report = tenants.map((t) => ({ id: t.id, name: t.name, rows: (byTenant.get(t.id) ?? []).length }));

4. One transaction per multi-step write#

saveChanges() is atomic on its own. When a unit spans several saves or raw SQL, wrap it in db.transaction(async (tx) => { … }); use executeUpdate/executeDelete for set-based writes instead of load-modify-save loops. Add rowVersion() to rows that several users edit and handle ConcurrencyError. See Transactions & Concurrency.

5. Define fields as methods#

Columns are builder methods, not constructor properties. Object-literal “fields” are silently ignored by the schema builder.

javascript
name(db) { db.string().notNullable(); }   // ✅
// this.name = { type: 'string' };         ❌ ignored

6. Name the context file after the class#

The migration CLI resolves contexts by file name. Keep AppContext in app/models/AppContext.js so master db can find it.

7. Use relative SQLite paths#

connection: 'db/' resolves under your project root and auto-creates the file. A leading slash (/db/) is treated as a filesystem-absolute path.

8. Parameterize — never concatenate#

Bind values with $$. The query builder parameterizes them, eliminating injection. The sql tag for executeUpdate refuses interpolations for the same reason.

javascript
db.User.where((u) => u.email == $$, email);   // ✅ safe & parameterized

9. Batch wisely#

Creating many rows? Save them through the same context and call saveChanges() once — MasterRecord builds an optimized batch insert that still runs your .set() transformers and relationships. Each save flushes only its dirty set, so a big tracked context is not a slow one.

10. Index your foreign keys and filters#

FK columns are indexed automatically. Add .index() to columns you filter or sort on frequently, and consider full-text indexes for searchable text.

11. Review migrations before applying#

masterrecord script AppContext prints the SQL update-database would run; watch for // POSSIBLE RENAME advisories (a rename is diffed as drop + add, as in EF) and swap in renameColumn before applying. Use update-database-all in deploy scripts — it exits non-zero when any context fails.

Quick reference
.new() create · .save() persist one · saveChanges() persist all · .where().toList() read many · .single()/.find() read one · .asNoTracking() read without tracking · .remove()/.delete() remove · db.transaction() span saves · close() dispose.