MasterRecord

Relationships

Connect your entities with expressive, code-first associations.

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.

models
// 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#

usage.js
// 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.

javascript
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.

models
// 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' }); }
usage.js
// 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:

javascript
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.*)):

javascript
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)
Custom foreign keys
Both 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.
Loading the other side
include(), thenInclude(), asSplitQuery(), explicit entry().load(), lazy await post.User and relationship fix-up are covered on Loading Related Data.