Optimizing Data Retrieval with AdonisJS Lucid ORM
Stop the N+1 query trap in AdonisJS. Learn how to use Lucid ORM's preloading and decorators to fetch complex relational data efficiently and type-safely.
02 Jan 2026, 08:11 UTC

The most common performance bottleneck in relational applications isn't the speed of the database itself, but the number of queries the application sends to it. Developers often encounter the "N+1 query problem," where a single request to fetch a list of records triggers dozens of subsequent queries to fetch related data for each item in that list.
AdonisJS solves this using the Lucid ORM, which implements the Active Record pattern. In this pattern, model classes represent database tables and instances represent rows. By combining TypeScript decorators with a strategic preloading mechanism, you can fetch complex relational graphs in a few optimized queries while maintaining strict type safety.
Defining Relationships via Decorators
Lucid uses decorators to define how models relate to one another. This allows the ORM to map foreign keys automatically and provides IDE autocomplete for related data. The primary relationship types include BelongsTo (many-to-one), HasMany (one-to-many), and ManyToMany (many-to-many).
For example, in a system where a User has many Posts, the models are defined as follows:
// User Model
import { Model, column } from '@ioc:Adonis/Lucid/Orm'
import { HasMany } from '@ioc:Adonis/Lucid/Relations'
import Post from './Post'
export default class User extends Model {
@column({ isPrimary: true })
public id: number
@column()
public username: string
@HasMany(() => Post)
public posts: HasMany<typeof Post>
}
// Post Model
import { Model, column } from '@ioc:Adonis/Lucid/Orm'
import { BelongsTo } from '@ioc:Adonis/Lucid/Relations'
import User from './User'
export default class Post extends Model {
@column({ isPrimary: true })
public id: number
@column()
public title: string
@column()
public userId: number
@BelongsTo(() => User)
public user: BelongsTo<typeof User>
}
Eliminating N+1 Queries with Preloading
If you fetch 50 posts and then loop through them to access post.user.username, Lucid would normally execute one query for the posts and 50 separate queries for the users. To prevent this, use the preload method. This instructs the ORM to fetch all related records using a single optimized query (typically using an IN clause) and map them back to the parent models in memory.
Run this logic within your controller or service layer:
// Fetch posts and their authors in only 2 queries
import Post from 'App/Models/Post'
export class PostsController {
public async index() {
const posts = await Post.query()
.preload('user')
.orderBy('created_at', 'desc')
return posts
}
}
Filtering Preloaded Data
Preloading is not limited to fetching entire sets. You can pass a callback to the preload method to filter the related records. This is essential when you only need a subset of data, such as fetching a user and only their "published" posts.
const users = await User.query().preload('posts', (postsQuery) => {
postsQuery.where('status', 'published').orderBy('created_at', 'desc')
})
Trade-offs and Memory Constraints
While Active Record is highly productive, it introduces a memory overhead. Because every row is instantiated as a full TypeScript class instance containing methods and lifecycle hooks, loading thousands of records can consume significantly more RAM than returning raw JSON objects.
If you are building a read-only report or an export feature involving thousands of rows, avoid full model instantiation. Instead, use the query builder's .select() method to return plain JavaScript objects, bypassing the model instantiation process entirely.
Practical Verification
To verify your implementation, enable SQL logging in your development environment. Check the console output to ensure that preload is resulting in a limited number of queries rather than a stream of repetitive SELECT statements. You can also validate relationship mapping by running node ace make:migration to ensure your database schema foreign keys align with the model definitions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.