Relationships between models
A post has many comments, and each comment belongs to one post. Such links between tables are called relationships. In Lucid you declare them in the models, and then you get a post's comments in one line, without hand-written SQL.
The comments table
Create the model together with a migration:
node ace make:model Comment -mIn the migration add a foreign key — a post_id column that points to the post:
// database/migrations/1727970000000_create_comments_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'comments'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.integer('post_id').unsigned().references('id').inTable('posts').onDelete('CASCADE')
table.text('body').notNullable()
table.timestamp('created_at')
table.timestamp('updated_at')
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}onDelete('CASCADE') deletes the comments together with the post. Run node ace migration:run.
The belongsTo relationship
A comment belongs to a post:
// app/models/comment.ts
import { DateTime } from 'luxon'
import { BaseModel, column, belongsTo } from '@adonisjs/lucid/orm'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
import Post from '#models/post'
export default class Comment extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare postId: number
@column()
declare body: string
@belongsTo(() => Post)
declare post: BelongsTo<typeof Post>
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
}From the relationship name post, Lucid finds the foreign key postId. The () => Post function lets models refer to each other.
The hasMany relationship
On the post side there are many comments. Add to the Post model:
// app/models/post.ts
import { BaseModel, column, hasMany } from '@adonisjs/lucid/orm'
import type { HasMany } from '@adonisjs/lucid/types/relations'
import Comment from '#models/comment'
export default class Post extends BaseModel {
// ...columns from the previous lessons
@hasMany(() => Comment)
declare comments: HasMany<typeof Comment>
}Preloading
Related records are loaded explicitly with preload:
// app/controllers/posts_controller.ts
async show({ params, view }: HttpContext) {
const post = await Post.query().where('id', params.id).preload('comments').firstOrFail()
return view.render('pages/posts/show', { post })
}{{-- resources/views/pages/posts/show.edge --}}
<h2>Comments ({{ post.comments.length }})</h2>
@each(comment in post.comments)
<p>{{ comment.body }}</p>
@endThe post page shows the list of its comments. If the model is already loaded, load the relationship like this: await post.load('comments').
Creating a related record
// app/controllers/comments_controller.ts
const post = await Post.findOrFail(params.postId)
await post.related('comments').create({ body: 'Great post!' })related('comments') fills in postId itself, you do not set it by hand.
Other relationships
| Decorator | Example |
|---|---|
hasOne | A user has one profile |
hasMany | A post has many comments |
belongsTo | A comment belongs to a post |
manyToMany | Posts and tags through a pivot table |
Common mistake
Accessing post.comments without preload or load does not query the database: the property is undefined, so the template either prints nothing or fails on post.comments.length. Lucid never loads relationships by itself — always preload the ones you need in the controller.