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:

  1. The Related model which is in this case Role.
  2. Intermediate pivot model which is in this case RoleUser.
  3. Field of the pivot model that holds the id value of the parent โ€“ User โ€“ model.
  4. 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.