Many To Many
Many-to-many relations are slightly more complicated than other relationships. An example of such a relationship is a user with many roles, where the roles are also shared by other users. For example, many users may have the role of "Admin". To define this relationship, three models are needed: User, Role, and RoleUser.
The RoleUser contains the fields to hold id of User and Role model. We'll define user_id
and role_id
fields here. The name of RoleUser model could be anything, but for this example, we'll keep it this way to make it easy to understand.
Many-to-many relationships are defined by defining this.belongsToMany()
.
class User extends Model {
static entity = 'users'
static fields () {
return {
id: this.attr(null),
roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id')
}
}
}
class Role extends Model {
static entity = 'roles'
static fields () {
return {
id: this.attr(null)
}
}
}
class RoleUser extends Model {
static entity = 'roleUser'
static primaryKey = ['role_id', 'user_id']
static fields () {
return {
role_id: this.attr(null),
user_id: this.attr(null)
}
}
}
The argument order of the belongsToMany
attribute is:
- The Related model which is in this case Role.
- Intermediate pivot model which is in this case RoleUser.
- Field of the pivot model that holds the id value of the parent – User – model.
- Field of the pivot model that holds the id value of the related – Role – model.
You may also define custom local key at 5th and 6th argument.
class User extends Model {
static entity = 'users'
static fields () {
return {
id: this.attr(null),
roles: this.belongsToMany(
Role,
RoleUser,
'user_id',
'role_id',
'user_local_id',
'role_local_id'
)
}
}
}
Defining The Inverse Of The Relationship
To define the inverse of a many-to-many relationship, you can place another belongsToMany
attribute on your related model. To continue our user roles example, let's define the users method on the Role model:
class User extends Model {
static entity = 'users'
static fields () {
return {
id: this.attr(null)
}
}
}
class Role extends Model {
static entity = 'roles'
static fields () {
return {
id: this.attr(null),
users: this.belongsToMany(User, RoleUser, 'role_id', 'user_id')
}
}
}
class RoleUser extends Model {
static entity = 'roleUser'
static primaryKey = ['role_id', 'user_id']
static fields () {
return {
role_id: this.attr(null),
user_id: this.attr(null)
}
}
}
As you can see, the relationship is defined the same as its User
counterpart, except referencing the User
model and the order of 3rd and 4th argument is reversed.
Access Intermediate Model
Working with many-to-many relations requires the presence of an intermediate model. Pinia ORM provides some helpful ways of interacting with this model. For example, let's assume our User
object has many Role
objects that it is related to. After accessing this relationship, we may access the intermediate model using the pivot
attribute on the models.
const user = User.query().with('roles').first()
user.roles.forEach((role) => {
console.log(role.pivot)
})
Notice that each Role
model we retrieve is automatically assigned a pivot
attribute. This attribute contains a model representing the intermediate model and may be used like any other model.
Customizing The pivot
Attribute Name
As noted earlier, attributes from the intermediate model may be accessed on models using the pivot
attribute. However, you are free to customize the name of this attribute to better reflect its purpose within your application.
For example, if your application contains users that may subscribe to podcasts, you probably have a many-to-many relationship between users and podcasts. If this is the case, you may wish to rename your intermediate table accessor to subscription
instead of pivot
. This can be done using the as
method when defining the relationship:
class User extends Model {
static entity = 'users'
static fields () {
return {
id: this.attr(null),
podcasts: this.belongsToMany(
Podcast,
Subscription,
'user_id',
'podcast_id'
).as('subscription')
}
}
}
Once this is done, you may access the intermediate table data using the customized name.
const user = useRepo(User).with('podcasts').first()
user.podcasts.forEach((podcast) => {
console.log(podcast.subscription)
})