Skip to content

Commit

Permalink
feat(orm): add docs for BelongsToMany
Browse files Browse the repository at this point in the history
  • Loading branch information
jlenon7 committed Jan 5, 2024
1 parent 354eb8e commit 52ca5e2
Show file tree
Hide file tree
Showing 3 changed files with 455 additions and 1 deletion.
158 changes: 157 additions & 1 deletion docs/orm/annotations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -573,5 +573,161 @@ public user: Relation<User>

## `@BelongsToMany()`

Comming soon...
The `@BelongsToMany()` annotation marks a model property as a
[many-to-many relation](/docs/orm/relationships#many-to-many):

```typescript
import { Role } from '#app/models/Role'
import type { Relation } from '@athenna/database'
import { RolesUsers } from '#app/models/RolesUsers'
import { Column, BaseModel, BelongsToMany } from '@athenna/database'

export class User extends BaseModel {
@Column()
public id: number

@BelongsToMany(() => Role, () => RolesUsers)
public roles: Relation<Role[]>
}
```

The third argument is an object that accepts any of the following
options:

### `isIncluded`

> Default: `false`
Set if the relation will be included when fetching data from the
parent model:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
isIncluded: true
})
public roles: Relation<Role[]>
```

:::tip

This option is used by methods like `with()` to eager load your
relation in the parent model:

```typescript
const user = await User.query()
.with('roles') // 👈 Set isIncluded to true in query schema.
.find()
```

This means that if you set this property to `true`, it will by
default load your relation every time.

:::

### `closure`

> Default: `undefined`
Set a closure that should be called when `isIncluded` is true,
adding to possibility to call methods from the query builder
of the relationship:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
isIncluded: true,
closure: (query) => query.select('id')
})
public roles: Relation<Roles[]>
```

:::tip

Just like [`isIncluded` property](/docs/orm/annotations#isincluded-3)
this option is used by methods like `with()` when eager loading your
relation in your parent model:

```typescript
const user = await User.query()
// Set isIncluded to true and add a select
// statement to the relationship query builder.
.with('roles', (query) => query.select('id'))
.find()
```

This means that if you set this property, it will by default be called
when loading your relation without the need to set a second argument to
the `with()` method.

:::

### `pivotTable`

> Default: Athenna will call the `table()` method of the pivot model.
> (e.g RolesUsers.table() -> `roles_users`)
Define the pivot table of the relationship:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
pivotTable: 'roles_users'
})
public roles: Relation<Role>
```

### `primaryKey`

> Default: the `isMainPrimary` of the parent model (e.g `id`).
Define the primary key of the relationship:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
primaryKey: 'myId'
})
public roles: Relation<Role>
```

### `foreignKey`

> Default: the parent model name in camel case
> with an `Id` at the end (e.g `userId`). This
> value needs to be defined in the pivot model
> (e.g RolesUsers).
Define the foreign key of the relationship:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
foreignKey: 'myUserId'
})
public roles: Relation<Role[]>
```

### `relationPrimaryKey`

> Default: the `isMainPrimary` of the relation model (e.g `id`).
Define the relation primary key of the relationship:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
relationPrimaryKey: 'myId'
})
public roles: Relation<Role>
```

### `relationForeignKey`

> Default: the relation model name in camel case
> with an `Id` at the end (e.g `roleId`). This
> value needs to be defined in the pivot model
> (e.g RolesUsers).
Define the relation foreign key of the relationship:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
relationForeignKey: 'myRoleId'
})
public roles: Relation<Role[]>
```
186 changes: 186 additions & 0 deletions docs/orm/relationships.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -339,5 +339,191 @@ public post: Relation<Post[]>

## Many To Many

Many-to-many relations are slightly more complicated than one-to-one
and one-to-many relationships. An example of a many-to-many relationship
is a user that has many roles and those roles are also shared by other
users in the application. For example, a user may be assigned the role
of "Author" and "Editor"; however, those roles may also be assigned to
other users as well. So, a user has many roles and a role has many users.

### Table structure

To define this relationship, three database tables are needed: `users`,
`roles`, and `roles_users`. The `roles_users` table is derived from the
alphabetical order of the related model names and contains `userId`
and `roleId` columns. This table is used as an intermediate table
linking the users and roles.

Remember, since a role can belong to many users, we cannot simply
place a `userId` column on the `roles` table. This would mean that a
role could only belong to a single user. In order to provide support
for roles being assigned to multiple users, the `roles_users` table
is needed. We can summarize the relationship's table structure like so:

```typescript
users
id - integer
name - string

roles
id - integer
name - string

roles_users
userId - integer
roleId - integer
```

### Model structure

Many-to-many relationships are defined by defining a property annotated
with the `@BelongsToMany()` annotation. But before we check how to define
this annotation, we need to first create our `RolesUsers` pivot model:

```typescript
import { Role } from '#app/models/Role'
import { User } from '#app/models/User'
import type { Relation } from '@athenna/database'
import { Column, BaseModel, BelongsTo } from '@athenna/database'

export class RolesUsers extends BaseModel {
@Column()
public id: number

@Column()
public roleId: number

@Column()
public userId: number

@BelongsTo(() => Role)
public role: Relation<Role>

@BelongsTo(() => User)
public user: Relation<User>

}
```

And now let's define the roles relation on our `User` model. The first
argument passed to the annotations is a closure that returns the related
model class and the second argument is a closure that returns the pivot
model:

```typescript
import { Role } from '#app/models/Role'
import type { Relation } from '@athenna/database'
import { RolesUsers } from '#app/models/RolesUsers'
import { Column, BaseModel, BelongsToMany } from '@athenna/database'

export class User extends BaseModel {
@Column()
public id: number

@Column()
public name: string

@BelongsToMany(() => Role, () => RolesUsers)
public roles: Relation<Role[]>
}
```

Once the relationship is defined, you may access the user's roles using
the `with()` or `load()` methods:

```typescript
const user = await User.query()
.with('roles', query => query.select('id'))
.find()

// Or

const user = await User.find()

await user.load('roles', query => query.select('id'))

console.log(user.roles)
```

To determine the table name of the relationship's pivot table,
Athenna will call the `table()` method of the pivot model. However, you
are free to override this convention. You may do so by passing a third
argument to the `@BelongsToMany()` annotation:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
pivotTable: 'roles_users'
})
public roles: Relation<Roles[]>
```

In addition to customizing the name of the pivot table, you may also
customize the column names of the keys on the table by passing additional
arguments to the options:

```typescript
@BelongsToMany(() => Role, () => RolesUsers, {
pivotTable: 'roles_users',
primaryKey: 'id',
foreignKey: 'userId',
relationPrimaryKey: 'id',
relationForeignKey: 'roleId'
})
public roles: Relation<Roles[]>
```

### Defining the inverse of the relationship

To define the "inverse" of a many-to-many relationship, is basically
the same process of defining on the parent model. To complete our
user / role example, let's define the `users` property on the `Role`
model:

```typescript
import { User } from '#app/models/User'
import type { Relation } from '@athenna/database'
import { RolesUsers } from '#app/models/RolesUsers'
import { Column, BaseModel, BelongsToMany } from '@athenna/database'

export class Role extends BaseModel {
@Column()
public id: number

@Column()
public name: string

@BelongsToMany(() => User, () => RolesUsers)
public users: Relation<User[]>
}
```

As you can see, the relationship is defined exactly the same as its `User`
model counterpart with the exception of referencing the `User` model.
Since we're reusing the `@BelongsToMany()` annotation, all of the usual
table and key customization options are available when defining the
"inverse" of many-to-many relationships.

### Retrieving pivot table data

As you have already learned, working with many-to-many relations requires
the presence of a pivot table and it pivot model, meaning that if you need
to retrieve the data from the pivot table, you can simply use the pivot model:

```typescript
const user = await User.find()
const role = await Role.find()

const rolesUsers = await RolesUsers.query()
.where('userId', user.id)
.where('roleId', role.id)
.findMany()
```

## Eager loading

Comming soon...

## Inserting & updating related models

Comming soon...

Loading

0 comments on commit 52ca5e2

Please sign in to comment.