Relationships
Relationships are declared like any other field — a method that calls a relationship builder. MasterRecord wires up the foreign keys, eager/lazy loading, and cascading for you.
belongsTo & hasMany#
A belongsTo creates and owns the foreign-key column; the inverse hasMany reads through it. belongsTo('User') automatically creates a user_id column.
// User.js
export default class User {
id(db) { db.integer().primary().auto(); }
name(db) { db.string(); }
Posts(db) { db.hasMany('Post'); } // one User → many Posts
}
// Post.js
export default class Post {
id(db) { db.integer().primary().auto(); }
title(db) { db.string(); }
User(db) { db.belongsTo('User'); } // creates user_id, links to User
}Using the association#
// Create a post for a user
const post = db.Post.new();
post.title = 'Eternia News';
post.user_id = user.id;
await post.save();
// Read related records
const author = await db.Post.where((p) => p.id == $$, post.id).include('User').single();
console.log(author.User.name);hasOne#
A one-to-one association. The related record is required unless the column is nullable.
export default class User {
id(db) { db.integer().primary().auto(); }
Profile(db) { db.hasOne('Profile'); }
}manyToMany (skip navigations)#
db.manyToMany('Tag') is EF Core 5+’s HasMany().WithMany(): the context synthesizes the join entity — named after the two entities in alphabetical order (PostTag), with an auto primary key, belongsTo to both sides (post_id, tag_id, FK constraints with ON DELETE CASCADE) and a unique composite index — and registers it through dbset(), so migrations, db.PostTag and the query builders all see it.
// Post.js
export default class Post {
id(db) { db.integer().primary().auto(); }
title(db) { db.string(); }
Tags(db) { db.manyToMany('Tag'); }
}
// Tag.js — declaring the reverse side is optional; it maps to the same join entity
export default class Tag {
id(db) { db.integer().primary().auto(); }
label(db) { db.string(); }
Posts(db) { db.manyToMany('Post'); }
}
// options: { through, foreignKey, otherKey } — self-referencing must pass both keys
// Followers(db) { db.manyToMany('User', { foreignKey: 'follower_id', otherKey: 'following_id' }); }// Insert: persisted targets linked by key, new targets inserted first (EF cascade insert)
const post = db.Post.new();
post.title = 'Grayskull';
post.Tags = [existingTag, otherTagId, { label: 'new' }];
await db.saveChanges();
// Load + edit the collection (EF Collection(n).Add / Remove) — persisted by saveChanges()
const tags = await post.Tags;
tags.add(anotherTag);
await tags.remove(existingTag);
await db.saveChanges();hasManyThrough#
A many-to-many through a join entity youdeclare (EF’s explicit join entity). Pass the join table and its foreign key:
export default class Post {
id(db) { db.integer().primary().auto(); }
Tags(db) { db.hasManyThrough('Tagging', 'tag_id'); }
}Explicit hasManyThrough navigations get the same collection add() / remove() API as manyToMany.
Cascading deletes & FK constraints#
Every belongsTo column gets a real FOREIGN KEY constraint (EF always creates them). ON DELETE follows the model: CASCADE by default — removing a parent removes its children — or opt out per association with .stopCascadeOnDelete() (→ SET NULL when nullable, RESTRICT when required), or choose explicitly with .onDelete('cascade' | 'restrict' | 'setNull' | 'noAction') (EF OnDelete(DeleteBehavior.*)):
Posts(db) { db.hasMany('Post').stopCascadeOnDelete(); }
User(db) { db.belongsTo('User').nullable().onDelete('setNull'); }
Legacy(db){ db.belongsTo('Legacy').excludeForeignKeyFromMigrations(); } // ORM-only, no DB constraint (EF 11)hasMany and belongsTo accept an explicit key as a second argument, e.g. db.hasMany('Post', 'author_id')— handy when your column doesn’t follow the <model>_id convention.include(), thenInclude(), asSplitQuery(), explicit entry().load(), lazy await post.User and relationship fix-up are covered on Loading Related Data.