A one-to-one relationship means one record in a table is associated with exactly one record in another table. Think of it like a person and their passport - each person has one passport, and each passport belongs to one person.
// User modelclass User extends Model { profile() { return this.hasOne('Profile', 'user_id'); }}// Profile model class Profile extends Model { user() { return this.belongsTo('User', 'user_id'); }}// Usageconst user = await User.with('profile').find(1);console.log(user.profile.bio);const profile = await Profile.with('user').find(1);console.log(profile.user.name);
String References: Always use string model names ('Profile') instead of importing classes to avoid circular dependencies.
A one-to-many relationship means one record can be associated with multiple records in another table. Think of it like a blog author and their posts - one author can write many posts, but each post belongs to only one author.
export default class CreatePostsTable { async up(schema) { await schema.createTable('posts', (table) => { table.increments('id'); table.string('title').notNullable(); table.text('content').notNullable(); table.integer('user_id').unsigned().notNullable(); table.boolean('is_published').defaultTo(false); table.timestamps(true, true); table.foreign('user_id').references('id').inTable('users').onDelete('CASCADE'); table.index('user_id'); // Index for better query performance }); }}
Model definitions:
// User modelclass User extends Model { posts() { return this.hasMany('Post', 'user_id'); }}// Post modelclass Post extends Model { author() { return this.belongsTo('User', 'user_id'); }}// Usageconst user = await User.with('posts').find(1);console.log(`${user.name} has ${user.posts.length} posts`);const post = await Post.with('author').find(1);console.log(`Written by: ${post.author.name}`);
A many-to-many relationship means multiple records in one table can be associated with multiple records in another table. Think of it like blog posts and tags - one post can have many tags, and one tag can be used on many posts. This requires a “pivot” or “junction” table to store the connections.
A polymorphic relationship allows one model to belong to multiple other model types. Every model involved must call static { this.register(); } so the ORM can resolve type strings at runtime.
A has-many-through relationship provides a shortcut to access distant relationships through an intermediate model. Think of it like getting all posts from a specific country - you go through users to get to their posts, but you want to skip the middle step.
// Country modelclass Country extends Model { users() { return this.hasMany('User', 'country_id'); } // Get all posts from users in this country posts() { return this.hasManyThrough('Post', 'User', 'country_id', 'user_id'); }}// User modelclass User extends Model { country() { return this.belongsTo('Country', 'country_id'); } posts() { return this.hasMany('Post', 'user_id'); }}// Post modelclass Post extends Model { author() { return this.belongsTo('User', 'user_id'); }}// Usageconst country = await Country.with('posts').find(1);console.log(`${country.name} has ${country.posts.length} posts`);// Get posts with author informationconst countryWithPosts = await Country.with('posts.author').find(1);
Enable strictLoading on a model to throw an error whenever a relation is accessed without being eager loaded. This catches N+1 problems at development time instead of silently returning undefined:
class Post extends Model { static strictLoading = true;}// ❌ This throws in developmentconst posts = await Post.all();posts[0].relations.comments; // Error: 'comments' was not eager loaded on Post// ✅ Always eager load when strictLoading is onconst posts = await Post.with('comments').get();posts[0].relations.comments; // works fine
Enable strictLoading during development to catch N+1 issues early, then remove or leave it enabled based on your team’s preference.
// Users who have postsconst authorsWithPosts = await User.has('posts').get();// Users with more than 5 postsconst prolificAuthors = await User.has('posts', '>', 5).get();// Users with published postsconst publishedAuthors = await User.whereHas('posts', (query) => { query.where('is_published', true);}).get();
When a child record saves, automatically bump the parent’s updated_at. Useful for cache invalidation — if a comment is added to a post, the post itself looks “recently updated”:
class Comment extends Model { static touches = ['post']; post() { return this.belongsTo('Post', 'post_id'); }}// Saving a comment will also UPDATE posts SET updated_at = NOW() WHERE id = comment.post_idawait comment.save();
You can touch multiple parents at once:
class Like extends Model { static touches = ['post', 'user']; post() { return this.belongsTo('Post'); } user() { return this.belongsTo('User'); }}
Only belongsTo relations are supported for touches. The parent must have timestamps enabled.
// ❌ Loads all posts just to countconst user = await User.with('posts').find(1);const postCount = user.posts.length;// ✅ Just countsconst user = await User.find(1);const postCount = await user.posts().count();