Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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 -m

In 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>
@end

The post page shows the list of its comments. If the model is already loaded, load the relationship like this: await post.load('comments').

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

DecoratorExample
hasOneA user has one profile
hasManyA post has many comments
belongsToA comment belongs to a post
manyToManyPosts 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.

Thanks for reading!

Donate to the site — we work hard for you.