Now that we've got our database populated with our schema using migrations, we're now ready to define our schema to Lucid using Models. Once we define our schema to Lucid, we'll be all set to start persisting and querying data from our database.
What Are Models?
Models are what we use to define our schema via TypeScript to Lucid. So, in most cases, we'll have a model for each table in our database. Then, within each model we'll describe the columns to Lucid, using TypeScript.
In addition to defining a table's columns via TypeScript in our models, we can also define computed properties, save hooks, query scopes, and serialization behavior. We'll dig into these later on.
Models Describe Relationships
We'll also use models to describe relationships to Lucid. Within our model, we can describe one-to-one, one-to-many, many-to-one, and many-to-many relationships. Thanks to this, in most cases we can skip creating models for intermediary tables altogether since they'll be described via the related model's relationship definition.
Models Can Mutate Data
We can also use models to persist, query, and delete data from the table the model represents. We can also reach through and do the same to related models as well. We'll be digging into this a little later in this series.
Creating Our First Model
So, let's go ahead and create our first model. Let's jump into our terminal and run:
$ node ace make:model User # CREATE: app/Models/User.ts
Copied!
This will create our User
model at /app/Models/User.ts
and it'll start us off with our User
class, which extends a BaseModel
from Adonis. Inside our User
model, Adonis will start us with some defaults; an id
primary key, a createdAt
column, and an updatedAt
column.
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 }
Copied!
- app
- Models
- User.ts
So, there are a few things to note here.
Adonis makes use of decorators as a way to describe a property. If the property is a column in the table we can use the
column
decorator.We define our primary keys using the
column
decorator. It's also worth noting we could do this outside the decorator as aprimaryKey
property on ourUser
model's class.As you can see with our
createdAt
andupdatedAt
properties, thecolumn
decorator can be used to describedateTime
, which also accepts whether the value should be auto-populated on insert and auto-updated on update.Adonis automatically serializes our snake-cased
created_at
andupdated_at
columns into a camel-case syntax. This behavior is consistent for all snake cased database columns.
Luxon Conversion
Adonis will also do something nice for us with our dateTime
defined columns. When we grab records from our database, Adonis will convert the DateTime strings from our database record into full DateTime Luxon objects.
Defining Our User
Now that we understand a little about what's going on in our model, let's continue onward to describing the remainder of our user columns. So, we'll define our username
, email
, password
, and remembermetoken
columns as:
export default class User extends BaseModel { @column({ isPrimary: true }) public id: number @column() public username: string @column() public email: string @column() public password: string @column() public rememberMeToken?: string @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime }
Copied!
- app
- Models
- User.ts
Now, since our remembermetoken
column is nullable, we'll want to reflect that in our model by making its property optional by using ?
. By doing this, we'll remind ourselves when we're using our model that this column may not have a value.
Securing Our Password
Another thing models are responsible for is defining how data is serialized. We can utilize this to make sure our password never reaches client-side code by defining our password's serializing as null.
@column({ serializeAs: null }) public password: string
Copied!
Next, we'll also want to ensure we're not storing plain text passwords in our database. To do this, we can make use of the beforeSave
hook within our model to hash our password, if it's been changed, anytime a user is created or updated.
import { beforeSave, /* ... */ } from '@ioc:Adonis/Lucid/Orm' @beforeSave() public static async hashPassword(user: User) { if (user.$dirty.password) { user.password = await Hash.make(user.password) } }
Copied!
It's also worth noting, had we already set up our authentication, Adonis would have taken care of this for us. I don't want to cover authentication until we have an understanding of controllers, migrations, models, and data persisting because authentication encapsulates all four concepts.
So, that should do it for now for our User
model. In the next lesson, we'll be expanding upon models further by learning how to define our relationships.
Save Hooks
So, similar to the beforeSave
save hook we utilized to hash our user's password anytime it's being changed in the database, we also have a number of other save hooks available to us.
beforeSave
called before a record is created or updatedafterSave
called after a record is created or updatedbeforeCreate
called before a record is createdafterCreate
called after a record is createdbeforeUpdate
called before a record is updatedafterUpdate
called after a record is updatedbeforeDelete
called before a record is deletedafterDelete
called after a record is deletedbeforeFind
called just before a single-item query is kicked-offafterFind
called just after a single-item query returnsbeforeFetch
called just before a multi-item query is kicked-offafterFetch
called just before a multi-item query returns
Computed Properties
Now, we don't need a computed property just yet for our User
model, but while we're learning about models I want to touch base with what they are and how to define them.
Computed properties are properties on our model that don't exist in our database at all, they aren't a column and they aren't related to anything else. They're properties we define and provide a value for within our models. For example, if you wanted to have an avatar for your user that's pulling from Gravatar, a computed property would be great for this! It would look something like the below.
import gravatar from 'gravatar' import { computed, /* ... other imports */ } from '@ioc:Adonis/Lucid/Orm' export default class User extends BaseModel { // ... other properties @column() public email: string @computed() public get avatar() { return gravatar.url(this.email) } }
Copied!
- app
- Models
- User.ts
Inside our computed property method, we'll have access to whatever user record is currently being propagated. Anytime we query a user or list of users, Lucid will compute our avatar
property and add it onto our user's data just the same as if it were actually stored in the database.
Computed Property Serialization
One thing to note here is that by providing the @computed
decorator we're telling Lucid we want this computed property to be serialized to JSON. If this isn't something you need or want for a computed property, it's perfectly valid to leave this off.
public get avatar() { return gravatar.url(this.email) }
Copied!
Defining Our Task & Project Models
So, now that we know the basics of models, let's continue onward by quickly filling out our Task
and Project
models.
Ace CLI Tip
In the past, we've been individually creating our models, controllers, and migrations as we need them. However, we can create all three for a single table in one blow using some flags on our Ace CLI commands.
Although we already have a migration creating our tasks
table, for demonstration purposes, let's create a model, controller, and migration for our tasks with a single command.
$ node ace make:model Task -mc # CREATE: database/migrations/TIMESTAMP_tasks.ts # CREATE: app/Controllers/Http/TasksController.ts # CREATE: app/Models/Task.ts
Copied!
As you can see, now we have a new migration, controller, and model for our tasks table. This is all thanks to two flags we added to the end of our make:model
command.
-m
designates that we'd like a migration with our model-c
designates that we'd like a controller with our model
If you run this command, please delete the newly generated tasks migration.
Additionally, if you actually inspect our newly created TasksController
you'll see Adonis started us off with a resource controller from the get-go.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class TasksController { public async index ({}: HttpContextContract) { } public async create ({}: HttpContextContract) { } public async store ({}: HttpContextContract) { } public async show ({}: HttpContextContract) { } public async edit ({}: HttpContextContract) { } public async update ({}: HttpContextContract) { } public async destroy ({}: HttpContextContract) { } }
Copied!
- app
- Controllers
- Http
- TasksController.ts
What about our case, where we already have our migrations setup? All we'd need to do is leave off the -m
from our command. So, for our projects, we could run the below to get just a model and controller.
$ node ace make:model Project -c # CREATE: app/Controllers/Http/ProjectsController.ts # CREATE: app/Models/Project.ts
Copied!
Again, since Adonis has context around the usage of our controller and knows it's bound to a model it'll start us off with a resource controller for our newly created ProjectsController
.
Task
For our Task
model, there will be just a couple of things to note. We have two relationships columns on our table, created_by
and assigned_to
. Although we'll be covering relationships in the next lesson, we can go ahead and define the id
representation of those relationships directly on our model by defining createdBy
and assignedTo
as numbers. We'll also want to make sure our assignedTo
is nullable since it's nullable in the database.
Additionally, we have a timestamp column on our database called due_at
. Unlike our created_at
and updated_at
columns, we won't want due_at
to be auto-assigned or auto-updated. This should specifically be user-defined. Therefore, we can just use @column.dateTime()
without an options object.
Your Task
models should end up looking like the below.
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class Task extends BaseModel { @column({ isPrimary: true }) public id: number @column() public createdBy: number @column() public assignedTo?: number @column() public name: string @column() public description?: string @column.dateTime() public dueAt?: DateTime @column() public statusId: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime }
Copied!
- app
- Models
- Task.ts
Project
As for our Project
model, there really isn't anything new going on here. It should end up looking like the following.
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class Project extends BaseModel { @column({ isPrimary: true }) public id: number @column() public name: string @column() public description?: string @column() public statusId: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime }
Copied!
- app
- Models
- Project.ts
Now for our Task
and Project
models, you might've noticed we're setting the statusId
to a type of number
. We can actually kick this up a notch, by defining our Status
enum and using that as the type for our statusId
. This will give us some nice TypeScript support for our statusId
in the future.
Status Enum
So, as you may recall from our directory structure lesson, the contracts
directory is a place where we can store TypeScript related items, like enums. So, for our Status
enum, let's go ahead and create a new folder inside our contracts
folder called enums
. Then, within the enums
folder create a new file called Status.ts
. This is where our Status
enum will reside.
Next, define your status enum as the following. We'll only have three statuses, but you can extend them later as you wish.
enum Status { IDLE = 1, IN_PROGRESS = 2, COMPLETE = 3 } export default Status;
Copied!
- contracts
- enums
- Status.ts
Like everything else in our project, we'll be able to conveniently import this by referencing the path to the file from our project root.
Now, let's apply our new Status
enum as our Task
and Project
statusId
type.
import Status from 'contracts/enums/Status' export default class Task extends BaseModel { // ... other fields @column() public statusId: Status // ... other fields }
Copied!
- app
- Models
- Task.ts
import Status from 'contracts/enums/Status' export default class Project extends BaseModel { // ... other fields @column() public statusId: Status // ... other fields }
Copied!
- app
- Models
- Project.ts
Great! That should get our models into a great state to move forward in the next lesson with relationships.
Next Up
So, we've learned how to define our models and some of the capabilities models bring to the table. In the next lesson, we'll learn how to define our relationships within our models. We'll also discuss when to use a one-to-one, one-to-many, or a many-to-many relationship and some of the options we have when defining these.
Join The Discussion! (10 Comments)
Please sign in or sign up for free to join in on the dicussion.
ad
Please sign in or sign up for free to reply
tomgobich
Please sign in or sign up for free to reply
claudio-barca
What about boolean types on Sqlite3 ?
Adding the serialize decorator
`
 @column({
  serialize: (value?: Number) => {
   return Boolean(value)
  },
 })
 public isClaimed: boolean
`
when I store the record on db it save 0/1 (because sqlite3 manage boolean as integers)
but when I get the value 0/1 from db it fail on the returned json.
I have a video to illustrate the problem but I can’t attached here.
Please sign in or sign up for free to reply
tomgobich
I’m not familiar with Sqlite at all, so there may be a better answer than what I’m about to provide. However, the serialize function is only going to be called when the query results are converted from Lucid data into JSON. This can occur by explicitly calling the
serialize()
method on a model record, stringifying, or returning the data as a JSON response.For this, I think it’d be better to use
prepare
andconsume
. Prepare will prepare the value for storage within the db. I’m not sure if this would be needed or if the db would auto transform true/false into 1/0. Consume will conversely transform the value when it’s queried from the db but before it’s set as the value on the model record.You can find the corresponding documentation for this here:
https://docs.adonisjs.com/reference/orm/decorators#column
Please sign in or sign up for free to reply
MrAvantiC
Hi Tom,
how would you handle the following case:
1. I have a resource that when created/updated receives a field in array/object structure as part of the request body:
2. Now, I defined this field in my model and migration like this:
3. At first I assumed that this would be enough and the field would automatically be serialized when attempting to save it to the database. However, I got errors at first. I could solve it by serializing the value manually like this:
Now I’m wondering if this is the correct way to handle JSON fields or if I’m missing some better solution here? :)
Please sign in or sign up for free to reply
tomgobich
Hey! So sorry, for some reason I thought I had answered this.
I have only worked with a JSON column in AdonisJS once so far, but I’ve had good luck defining its column in the model like so, note my:
This may not be the best way, I want to investigate this further and create a lesson on it as I’ve seen many questions about how to handle it and everyone seems to have a different answer.
However, in my case, by providing
object
as a type on the column, I’ve found the item itself is automatically parsed when I query the model. For example, my JSON column is calledbodyBlocks
. Below is a Repl output for the column when queried.Same, when it comes to setting the value. Since it’s an
object | string
I can set the value as either stringified or a non-stringified object.Hope this helps, and again sorry for the delayed response!
Please sign in or sign up for free to reply
MrAvantiC
Hey Tom, thank you for your response!
I tried to change my models as you suggested but I still can’t get it to work without the
beforeSave()
workaround. :(I created a minimal example of the issue: https://github.com/MrAvantiC/Adonis-JSON-serialization. Issue can be seen with
curl --location --request POST 'localhost:3333/test'
As long as I keep the
beforeSave()
hook inside the model, the column gets serialized just fine……however, when the record is returned in the response, for example right after creating it, the column also doesn’t get transformed back to JSON but is returned as a string.
I also tried to change the column type from
Array<object>
to just<object>
but that doesn’t change anything.I do know that other frameworks/ORMs support a way of automatically encoding/decoding data (e.g. https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#json) and I’m not sure what I’m missing here with Adonis… ^^
Please sign in or sign up for free to reply
tomgobich
Okay, so it looks like what I was seeing is something KnexJS is doing and not AdonisJS and it seems to only work when the variable root is an object, doesn’t seem to care what’s inside beyond that.
For example, if you change your data to:
It seems to insert a-okay without needing to stringify.
I played around with it for a bit and search online for a bit and everyone with arrays at the root level seems to be stringifying prior to inserting.
So, if you do need
triggerItems
to be an array, you can use the@beforeSave
hook. Alternatively, you can add to the column decorator aprepare
method. This is a property-specific version of the@beforeSave
hook.So, after testing, one benefit of using
prepare
over the@beforeSave
hook to stringify your array is that you can remove thestring
type off your model property since you no longer need to set a stringified item onto the property directly, instead, Adonis will take care of it using yourprepare
method as a callback.Lastly, I just tested this as well, you can give yourself intellisense on
triggerItems
by directly setting the type.Please sign in or sign up for free to reply
MrAvantiC
Hey Tom,
thanks for your effort to evaluate this!
I like your approach better than the
beforeSave()
hook, so I refactored it like this:I also tried to pass just the function reference…
…but then Typescript isn’t happy, even though it still works.. ;)
It would be great if there was some built-in function to achieve this. When researching, I found a package that seems to achieve something similar: https://github.com/watzon/adonis-jsonable
But it hasn’t received updates for quite a while, so I was initially assuming that Adonis now does this out-of-the-box. Guess not. ;)
Thanks for your help - again! :)
Please sign in or sign up for free to reply
tomgobich
Anytime! Happy you were able to settle on a solution :). I’m sure a package will come along in the future that’ll help with this!
Please sign in or sign up for free to reply