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 easy 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 particular use cases.
What is the 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 any programming language or framework can implement.
Whenever we say the term active record, we are talking about the pattern itself and not the implementation of Rails.
The active record pattern advocates encapsulating the database interactions to language-specific objects or classes. Each database table gets its model, and each instance of that class represents a table row.
The data models clean up many 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 set up , 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 defining the -m
flag.
node ace make:model User -m
# CREATE: database/migrations/1618903673925_users.ts
# CREATE: app/Models/User.ts
Finally, you can also create the factory for the model using the -f
flag.
node ace make:model User -f
# CREATE: app/Models/User.ts
# CREATE: database/factories/User.ts
The make:model
command creates a new model inside the 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 about the following error.
Points to note
- The
@column
decorator is used to distinguish between the standard class properties and the database columns. - We keep the models lean and do not define database-specific constraints, data types and triggers inside models.
- Any option you define inside the models does not change/impact the database. You must use migrations for that.
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 intended to query the database or insert new records.
Defining columns
Now that you are aware of 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.
- The
isPrimary
option marks the property as the primary key for the given database table. - The
serializeAs: null
option removes the property when you serialize the model to JSON. - View all available options
accepted by the
@column
decorator.
Date columns
Lucid further enhances the date and the date-time properties and converts 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 creation 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({ isPrimary: 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({ isPrimary: 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 only defines a static connection name that remains the same throughout the application's lifecycle.
export default class User extends BaseModel {
public static connection = 'pg'
}
FAQs
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 the database schema.
- Generating database tables from models means defining all database-level constraints and config within the models. This adds unnecessary bloat to the models.
- 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. Instead, 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 will use App/Something/ModelName
to load your model.
Additional reading
- Make sure to read the models reference guide .
- CRUD operations using models.
- How to serialize models to JSON.