Preloads
.preload("relation") eager-loads related records declared in your model annotations. All four relation kinds are supported. Records are batch-fetched with a single IN query per relation — no N+1.
Basic preload
// .preload("relation") — eager-load a declared relation
// Batch-fetched in a single IN query — no N+1
const posts = await Post.query()
.preload('user') // loads each post's user
.get();
// posts[0].user is the full User object
console.log(posts[0].user?.name);One-to-many
// onetomany — loads child records per parent
// @relation onetomany:Comment;foreignKey:postId
const posts = await Post.query()
.preload('comments')
.get();
// posts[0].comments is Comment[]Many-to-one
// manytoone — loads the parent record
// @relation manytoone:User;foreignKey:userId
const comments = await Comment.query()
.preload('user')
.get();
// comments[0].user is the User objectOne-to-one
// onetoone — loads the single related record
// @relationship onetoone:Profile;foreignKey:userId
const users = await User.query()
.preload('profile')
.get();
// users[0].profile is Profile | nullMany-to-many
Many-to-many relations fetch pivot data in a single INNER JOIN query against the through table.
// manytomany — uses a single INNER JOIN on the pivot table
// @relation manytomany:Team;through:team_members;foreignKey:userId;relatedKey:teamId
const users = await User.query()
.preload('teams')
.get();
// users[0].teams is Team[]Nested preloads
Use dot-notation to preload at any depth. Cycle detection prevents infinite loops.
// Nested preloads — dot-notation, any depth
const posts = await Post.query()
.preload('user') // loads post.user
.preload('user.profile') // loads post.user.profile
.get();
// Cycle detection is built-in — circular chains stop automatically
// Another example: posts with comments and each comment's author
const detailed = await Post.query()
.preload('comments')
.preload('comments.user')
.get();exclude() on preloads
Strip sensitive columns from root or any nested preloaded relation using dot-notation in exclude().
// .exclude() strips columns from preloaded relations
const posts = await Post.query()
.preload('user')
.exclude('user.password')
.exclude('user.secretToken')
.get();
// posts[0].user exists but has no .password or .secretToken field
// exclude() on root and nested in one chain
const users = await User.query()
.preload('posts')
.preload('posts.comments')
.exclude('password') // root User
.exclude('posts.body') // strip from Post
.exclude('posts.comments.ip') // strip from nested Comment
.get();How batching works
For a one-to-many preload, SlintORM collects all parent IDs from the root query result, then fires one WHERE id IN (...) query and distributes records back to their parents. This means N posts with comments = 2 queries total, not N+1.