Introduction

Along with the Database query builder, Lucid also has data models, built on top of the Active record pattern .

The data models layer of Lucid makes it super simple to perform CRUD operations, manage relationships between models, and define lifecycle hooks.

We recommend using models extensively and reach for the standard query builder for very specific use cases.

What is Active record pattern?

Active Record is also the name of the ORM used by Ruby on Rails. However, the Active record pattern is a broader concept that can be implemented by any programming language or framework.

Whenever we say the term Active record, we are pointing towards the pattern itself and not the implementation of Rails.

The Active record pattern advocates to encapsulate the database interactions to language specific objects or classes. Each database table gets its own model and each instance of that class represents a table row.

The data models cleans up a lot of database interactions, since you can encode most of the behavior inside your models vs writing it everywhere inside your codebase.

For example: Your users table has a date field and you want to format that before sending it back to the client. This is how your code may look like without using data models.

import { DateTime } from 'luxon'
const users = await Database.from('users').select('*')
return users.map((user) => {
user.dob = DateTime.fromJSDate(user.dob).toFormat('dd LLL yyyy')
return user
})

When using data models, you can encode the date formatting action within the model vs writing it everywhere you fetch and return users.

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
class User extends BaseModel {
@column.date({
serialize: (value) => value.toFormat('dd LLL yyyy')
})
public dob: DateTime
}

And use it as follows:

const users = await User.all()
return users.map((user) => user.toJSON()) // date is formatted during `toJSON` call

Creating your first model

Assuming you already have lucid setup , run the following command to create your first data model.

node ace make:model User
# CREATE: app/Models/User.ts

You can also generate the migration alongside the model by passing the -m flag.

node ace make:model User -m
# CREATE: database/migrations/1618903673925_users.ts
# CREATE: app/Models/User.ts

The make:model command creates a new model inside app/Models directory. Every model must extend the BaseModel class to inherit additional functionality.

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
export default class User extends BaseModel {
@column({ isPrimary: true })
public id: number
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}

Columns

You will have to define your database columns as properties on the class and decorate them using the @column decorator.

Since, AdonisJS uses TypeScript, there is no way to get around WITHOUT defining the columns explicitly on the class, otherwise the TypeScript compiler will complain with the following error.

Points to note

To summarize the above points - Lucid maintains a clear separation between migrations and the models. Migrations are meant to create/alter the tables, and models are meant to query the database or insert new records.


Defining columns

Now that you are aware about the existence of columns on the model class. Following is an example of defining the user table columns as properties on the User model.

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
export default class User extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
public username: string
@column()
public email: string
@column({ serializeAs: null })
public password: string
@column()
public avatarUrl: string | null
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}

The @column decorator additionally accepts options to configure the property behavior.


Date columns

Lucid further enhances the date and the date time properties and convert the database driver values to an instance of luxon.DateTime .

All you need to do is make use of the @column.date or @column.dateTime decorators and Lucid will handle the rest for you.

@column.date()
public dob: DateTime
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime

Optionally, you can pass the autoCreate and autoUpdate options to always define the timestamps during the create and the update operations. Do note, setting these options doesn't modify the database table or its triggers.


Column names

Lucid assumes that your database columns names are defined as snake_case and automatically converts the model properties to snake case during database queries. For example:

await User.create({ avatarUrl: 'foo.jpg' })
// EXECUTED QUERY
// insert into "users" ("avatar_url") values (?)

Overwrite column names globally

If you are not using the snake_case convention in your database, then you can override the default behavior of Lucid by defining a custom Naming Strategy

Overwrite column names inline

You can also define the database column names explicitly within the @column decorator. This is usually helpful for bypassing the convention in specific use cases.

@column({ columnName: 'user_id', isPrimary: true })
public id: number

Models config

Following are the configuration options to overwrite the conventional defaults.

primaryKey

Define a custom primary key (defaults to id). Setting the primaryKey on the model doesn't modify the database. Here, you are just telling Lucid to consider id as the unique value for each row.

class User extends Basemodel {
public static primaryKey = 'email'
}

Or use the primaryKey column option

class User extends Basemodel {
@column({ primaryKey: true })
public email: string
}

table

Define a custom database table name. Defaults to the plural and snake case version of the model name.

export default class User extends BaseModel {
public static table = 'app_users'
}

selfAssignPrimaryKey

Set this option to true if you don't rely on the database to generate the primary keys. For example: You want to self assign uuid to the new rows.

import uuid from 'uuid/v4'
import { BaseModel, beforeCreate } from '@ioc:Adonis/Lucid/Orm'
export default class User extends BaseModel {
public static selfAssignPrimaryKey = true
@column({ primaryKey: true })
public id: string
@beforeCreate()
public static assignUuid(user: User) {
user.id = uuid()
}
}

connection

Instruct model to use a custom database connection defined inside the config/database file.

DO NOT use this property to switch the connection at runtime. This property is only to define a static connection name that remains same through out the lifecycle of the application.

export default class User extends BaseModel {
public static connection = 'pg'
}

FAQ's

Does models creates the database tables automatically?

No. We do not sync your models with the database. Creating/altering tables must be done using migrations . Here are some of the reasons for not using models to create database schema.

  1. Generating database tables from models means, we need to define all database level constraint and config within the models. This adds unnecessary bloat to the models.
  2. Not every database change is as simple as renaming a column. There are scenarios, in which you want to migrate data from one table to another during re-structuring and this cannot/should not be expressed within models.
I am coming from TypeORM, how should I define column types?

We do not express database types inside models. We follow the approach of lean models and keep database level config within migrations.

Can I move my Models somewhere else?

Yes. You are free to put your model wherever you want! If your models are inside the app/Something folder, you would use App/Something/ModelName to load your model.

Additional reading