Defining relationships within our models unlocks a whole lot of power when it comes to querying and persisting data within our database. We'll be able to reference our relationships using related models without needing to define queries for each related model we need to populate.
Model Relationship Options
Within our model, we'll use both decorators and types to define a relationship. Adonis provides a decorator and a type for each type of relationship.
Decorator:
@hasOne
Type:HasOne
Purpose: Used to define a one-to-one relationshipDecorator:
@hasMany
Type:HasMany
Purpose: Used to define a many-to-one relationshipDecorator:
@belongsTo
Type:BelongsTo
Purpose: Used to define the inverse of aHasOne
orHasMany
relationshipDecorator:
@manyToMany
Type:ManyToMany
Purpose: Used to define a many-to-many relationship
So, to start, let's first understand the relationships we need to define for our users, then we'll take a look at the options Lucid provides to us, and finally, we'll put the two together.
Defining Our User's Relationships
If we inspect our User
model you'll see we don't have a single relatable column defined. However, if we inspect our other models you'll see both Project
and Task
contain columns that related back to our User
. Let's walk through these relationships and their relationship types.
tasks
.created_by
This will be a many-to-one relationship with our User. Our users can create many tasks, but a task can only be created by one user.tasks
.assigned_to
This will also be a many-to-one relationship with our User. Our users can have many tasks assigned to them, but a task can only be assigned to one user.project_users
.user_id
This will be a many-to-many. A user can be a member of many projects and a project will have many users.
How Do I Know Which To Use?
Recalling my college days, understanding database relationship types were always a struggling point. That's why I really like the way Adonis handles its relationship naming. In most cases, in non-technical terms, we can describe our relationships and have them correlate pretty closely to one of Adonis' relationship names.
So, let's walk through one of our relationships here to see how to determine which relationship type to use for a given relationship.
Describe your relationship in non-technical terms.
My users can have many tasks, and a task belongs to a specific user.Ask yourself which bucket your description fits into
Since our user has many tasks but a task can only have one user, we can discern this is a many-to-one relationship type. If we pay attention to the terms we used to describe our relationship, we can also see these correlate pretty darn close to Adonis' relationship verbiage."My user can have many tasks" - So we'll use
HasMany
to define this relationship from ourUser
."My task belongs to a specific user" - So we'll use
BelongsTo
to define the relationship from ourTask
.Then we'd need to define both sides of this relationship, using
HasMany
on ourUser
model andBelongsTo
on ourTask
model.
One thing I've found that is typically true is when a model contains an id column for the relationship it's usually the side that gets the BelongsTo
. So, for the user-to-task relationship we just walked through, our createdBy
column is on the Task
model. Therefore, it'll be the side that gets the BelongsTo
relationship definition. Note that for many-to-many relationships that's never the case since both sides will use ManyToMany
.
How To Define A Relationship
Now that we've described our user's relationships and what our relationship options are, let's learn how to define our relationships. As I mentioned, to define a relationship we'll need to use both a decorator and a type for the relationship we need. So, for our user's relationship with task's created_by
column, we would define the many-to-one relationship like so:
import { hasMany, HasMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import Task from 'App/Models/Task'
export default class User extends BaseModel {
/* other fields */
@hasMany(() => Task, {
foreignKey: 'createdBy'
})
public tasks: HasMany<typeof Task>
}
So, as you can see we use the hasMany
decorator to define the relationship type. We'll then pass a callback function with the model we're relating it to. Then we use the HasMany
type, which we pass the typeof
the model we're relating it to.
Now, here we're also passing an object as our decorator's second parameter with a foreignKey
property. By default, Lucid will look for a userId
property on our Task
model to define the relationship. It'll default to a camel-cased concatenation of the model's name and the model's defined primary key column, which is typically an id
. For this use case, it would default to looking for a userId.
However, this is not the case with our schema. This relationship is using the createdBy
property on our Task
model to define the related user. Therefore, we need to explicitly define the key the relationship needs to use. We use foreignKey
to describe this here because the createdBy
column exists on the other model, not our User
.
Now that we have one side of our relationship defined, we need to hop over to our Task
model and define the other side of the relationship. This will also let Lucid know our relationship context regardless if we're pulling the relationship from our User
or our Task
.
import { column, belongsTo, BelongsTo, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import User from 'App/Models/User'
export default class Task extends BaseModel {
/* other fields */
@column()
public createdBy: number
@belongsTo(() => User, {
foreignKey: 'createdBy'
})
public user: BelongsTo<typeof User>
}
Just like with our User
side of the relationship, Lucid will default to looking for a camel-cased concatenation of the related model name and its defined primary key column name, userId
in our case. Since we're not using userId
to describe this relationship, we need to manually define which property to use.
I'd also like to note that if our task's used a userId
column to define the relationship, we wouldn't need to explicitly define a localKey
or foreignKey
on our relationship, since this is the default behavior. So, for example, our relationship would've looked like this:
export default class Task extends BaseModel {
@column()
public userId: number
@belongsTo(() => User)
public user: BelongsTo<typeof User>
}
The code structure here is the same whether you're defining a HasMany
, HasOne
, BelongsTo
, or ManyToMany
relationship. Additionally, as we did with our createdBy
relationships, we can explicitly define the relationship fields for each as well. As you'll see a little later in this lesson, we can also define which intermediary table (pivot table) to use and which columns to auto-populate for our ManyToMany
relationships when we query the relationship.
Completing User Relations
So, we have one of our User
relationships done and dusted, let's wrap up the remaining. Our assignedTo
relationship is going to be practically identical to our createdBy
, so let's define that next.
Assigned To (Many-To-One)
Let's jump back into our User
model, and let's copy and paste our tasks
definition and change the name of the foreignKey
from createdBy
to assignedTo
for our newly pasted definition. Now, whenever it comes to the naming of our relationship properties, we're welcome to use whatever makes sense. This is thanks to the relationship itself being defined by the decorator. However, we can't have duplicates, since that would lead to overwriting a relationship. So, for our assignedTo
definition let's change the property name from tasks
to assignedTasks
.
import { hasMany, HasMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import Task from 'App/Models/Task'
export default class User extends BaseModel {
/* other fields */
@hasMany(() => Task, {
foreignKey: 'createdBy'
})
public tasks: HasMany<typeof Task>
@hasMany(() => Task, {
foreignKey: 'assignedTo'
})
public assignedTasks: HasMany<typeof Task>
}
Now our User
has a relationship defined for both tasks the user has created and tasks the user is assigned to.
Next, let's define the other side of this relationship. So, let's jump over to our Task
model. Again, let's copy and paste our user
property relationship definition and change the foreignKey
from createdBy
to assignedTo
.
import { column, belongsTo, BelongsTo, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import User from 'App/Models/User'
export default class Task extends BaseModel {
/* other fields */
@column()
public createdBy: number
@column()
public assignedTo: number
@belongsTo(() => User, {
foreignKey: 'createdBy'
})
public user: BelongsTo<typeof User>
@belongsTo(() => User, {
foreignKey: 'assignedTo'
})
public user: BelongsTo<typeof User>
}
Again, we can change these property names for our relationships to whatever makes sense. So, here it would probably make the most sense to change our createdBy
's user property name to creator
and our assignedTo
's user property name to assignee
.
import { column, belongsTo, BelongsTo, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import User from 'App/Models/User'
export default class Task extends BaseModel {
/* other fields */
@column()
public createdBy: number
@column()
public assignedTo: number
@belongsTo(() => User, {
foreignKey: 'createdBy'
})
public creator: BelongsTo<typeof User>
@belongsTo(() => User, {
foreignKey: 'assignedTo'
})
public assignee: BelongsTo<typeof User>
}
User Projects (Many-To-Many)
The last relationship we need to define for our User
is the many-to-many relationship for our user-to-project relationship.
In our User
model, very similar to our other relationships, let's define a relationship for a projects
property using the @manyToMany
decorator and the ManyToMany
type.
import { hasMany, HasMany, manyToMany, ManyToMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import Task from 'App/Models/Task'
import Project from 'App/Models/Project'
export default class User extends BaseModel {
/* other fields */
@hasMany(() => Task, {
foreignKey: 'createdBy'
})
public tasks: HasMany<typeof Task>
@hasMany(() => Task, {
foreignKey: 'assignedTo'
})
public assignedTasks: HasMany<typeof Task>
@manyToMany(() => Project)
public projects: ManyToMany<typeof Project>
}
Now, by default on many-to-many relationships adonis will lowercase and alpha sort the two related model names, then snake case the two. This results in the table name Adonis will use to search for the intermediary table joining the many-to-many relationship.
So, here Adonis will default to looking for a table called project_users
. Thankfully, that's what we're using. However, if that weren't what we were using we could explicitly define which intermediary to use via a pivotTable
property on our relationship's options.
CORRECTION
Adonis will actually default to looking for the singular form of our snake-cased table concatenation instead of a plural form. So, Adonis will actually default to looking for a table called project_user
. In a later lesson, we'll correct our migration to account for this.
export default class User extends BaseModel {
/* other fields */
@manyToMany(() => Project, {
pivotTable: 'project_users'
})
public projects: ManyToMany<typeof Project>
}
For this relationship, Adonis will look for four different columns between these three tables. All of which can be explicitly defined in the relationship's options object.
localKey
Purpose: An id column that's local (on the same model) to the relationship definition.
Usage:localKey: 'id'
Default: Model's defined primary key, which is by defaultid
.relatedKey
Purpose: An id column for the related table.
Usage:relatedKey: 'id'
Default: Model's defined primary key, which is by defaultid
.pivotForiegnKey
Purpose: An id column on the pivot table that relates to thelocalKey
table.
Usage:pivotForeignKey: 'user_id'
Default: Snake-cased concatenation of the model name and the model's primary key. For our use case, the default here would beuser_id
.pivotRelatedForeignKey
Purpose: An id column on the pivot table that relates to therelatedKey
table.
Usage:pivotForeignKey: 'project_id'
Default: Snake-cased concatenation of the model name and the model's primary key. For our use case, the default here would beproject_id
Additionally, we can also define columns on the intermediary table (pivotTable
) that we'd like to be included every time we query the many-to-many relationship. We can do this by an array of the columns we'd like to include to the pivotColumns
property on our relationship definition's options.
The full rundown of all these many-to-many options would look like the below. Thankfully, in most cases, a lot or all of these will make use of the default values and won't need explicitly defined.
export default class User extends BaseModel {
/* other fields */
@manyToMany(() => Project, {
pivotTable: 'project_users',
pivotColumns: ['role_id'],
pivotForeignKey: 'user_id',
pivotRelatedForeignKey: 'project_id',
localKey: 'id',
relatedKey: 'id'
})
public projects: ManyToMany<typeof Project>
}
For our use-case, all of these are default except for the pivotColumns
, so we can get rid of everything but the pivotColumns
option on our user's projects relationship definition. So, at the end of the day here, our User
model relationships should look like this:
import { hasMany, HasMany, manyToMany, ManyToMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import Task from 'App/Models/Task'
import Project from 'App/Models/Project'
export default class User extends BaseModel {
/* other fields */
@hasMany(() => Task, {
foreignKey: 'createdBy'
})
public tasks: HasMany<typeof Task>
@hasMany(() => Task, {
foreignKey: 'assignedTo'
})
public assignedTasks: HasMany<typeof Task>
@manyToMany(() => Project, {
pivotColumns: ['role_id']
})
public projects: ManyToMany<typeof Project>
}
Lastly, we just need to define the inverse side of this relationship on our Project
model in the same fashion.
import { ManyToMany, manyToMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import User from './User'
export default class Project extends BaseModel {
/* other fields */
@manyToMany(() => User, {
pivotColumns: ['role_id']
})
public users: ManyToMany<typeof User>
}
Project Tasks (Many-To-Many)
Our last relationship is for our project tasks, and similar to our project users, this will also be a many-to-many. So, while we're in our Project
model, let's go ahead and define our many-to-many relationship to our tasks.
import { ManyToMany, manyToMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import User from './User'
import Task from './Task'
export default class Project extends BaseModel {
/* other fields */
@manyToMany(() => User, {
pivotColumns: ['role_id']
})
public users: ManyToMany<typeof User>
@manyToMany(() => Task, {
pivotColumns: ['sort_order']
})
public tasks: ManyToMany<typeof User>
}
Nothing new here that we didn't cover with our project users, the main difference is we'll want to include the sort_order
column from our intermediary table.
Next, let's define the inverse side of this many-to-many relationship on our Task
model.
import { column, belongsTo, BelongsTo, manyToMany, ManyToMany, /* ... */ } from '@ioc:Adonis/Lucid/Orm'
import User from 'App/Models/User'
export default class Task extends BaseModel {
/* other fields */
@column()
public createdBy: number
@column()
public assignedTo: number
@belongsTo(() => User, {
foreignKey: 'createdBy'
})
public creator: BelongsTo<typeof User>
@belongsTo(() => User, {
foreignKey: 'assignedTo'
})
public assignee: BelongsTo<typeof User>
@manyToMany(() => Project, {
pivotColumns: ['sort_order']
})
public projects: ManyToMany<typeof Project>
}
Next Up
We should now have all of our relationships defined without our models. Next up is learning how to make use of them. However, before we learn how to make use of relationships, we must first learn how to make use of the models themselves. In the next lesson, we'll do just that, by learning how to persist data into our database using models.
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!