| sidebar_position | 5 |
|---|---|
| title | BelongsToMany |
The BelongsToMany association is used to create a Many-To-Many relationship between two models.
In a Many-To-Many relationship, a row of one table is associated with zero, one or more rows of another table, and vice versa.
For instance, a person can have liked zero or more Toots, and a Toot can have been liked by zero or more people.
erDiagram
people }o--o{ toots : likedToots
Because foreign keys can only point to a single row, Many-To-Many relationships are implemented using a junction table (called through table in Sequelize), and are really just two One-To-Many relationships.
erDiagram
people }o--|| liked_toots : user
liked_toots ||--o{ toots : toot
The junction table is used to store the foreign keys of the two associated models.
Here is how you would define the Person and Toot models in Sequelize:
import { Model, InferAttributes, InferCreationAttributes, NonAttribute } from '@sequelize/core';
import { BelongsToMany } from '@sequelize/core/decorators-legacy';
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
// highlight-start
@BelongsToMany(() => Toot, {
through: 'LikedToot',
})
declare likedToots?: NonAttribute<Toot[]>;
// highlight-end
}
class Toot extends Model<InferAttributes<Toot>, InferCreationAttributes<Toot>> {}In the example above, the Person model has a Many-To-Many relationship with the Toot model, using the LikedToot junction model.
The LikedToot model is automatically generated by Sequelize, if it does not already exist,
and will receive the two foreign keys: userId and tootId.
:::caution String through option
The through option is used to specify the through model, not the through table.
We recommend that you follow the same naming conventions as other models (i.e. PascalCase & singular):
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
// You should name this LikedToot instead.
// error-next-line
through: 'liked_toots',
})
declare likedToots?: NonAttribute<Toot[]>;
}:::
The junction table can be customized by creating the model yourself, and passing it to the through option.
This is useful if you want to add additional attributes to the junction table.
import {
Model,
DataTypes,
InferAttributes,
InferCreationAttributes,
NonAttribute,
} from '@sequelize/core';
import { BelongsToMany, Attribute, NotNull } from '@sequelize/core/decorators-legacy';
import { PrimaryKey } from './attribute.js';
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
through: () => LikedToot,
})
declare likedToots?: NonAttribute<Toot[]>;
}
class LikedToot extends Model<InferAttributes<LikedToot>, InferCreationAttributes<LikedToot>> {
declare likerId: number;
declare likedTootId: number;
}
class Toot extends Model<InferAttributes<Toot>, InferCreationAttributes<Toot>> {}In TypeScript, you need to declare the typing of your foreign keys, but they will still be configured by Sequelize automatically.
You can still, of course, use any attribute decorator to customize them.
When creating an N:M association, for example, with User and Project through UserProject you might want extra attributes on the junction table like the "role" attribute. This relationship can be set up like this:
class UserProject extends Model<InferAttributes<UserProject>, InferCreationAttributes<UserProject>> {
@Attribute(DataTypes.STRING)
declare role: string;
declare projectId: number;
declare userId: number;
}
class Project extends Model<InferAttributes<Project>, InferCreationAttributes<Project>> {
@Attribute(DataTypes.STRING)
declare name: string;
declare UserProject?: NonAttribute<UserProject>;
}
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
@Attribute(DataTypes.STRING)
declare username: string;
@BelongsToMany(() => Project, {
through: () => UserProject,
})
declare projects?: NonAttribute<Project[]>;
declare getProjects: BelongsToManyGetAssociationsMixin<Project>;
declare setProjects: BelongsToManySetAssociationsMixin<Project, number>;
declare addProjects: BelongsToManyAddAssociationsMixin<Project, number>;
}Creating multiple associations with the same extra attributes is possible by passing a single object on the through attribute:
user1.setProjects([project1, project2, project3], { through: { role: 'admin' }})With the set and add mixins, different extra attributes per association can be set by passing an array of objects of the same length as the number of associations:
user1.setProjects([project1, project2, project3], {
through: [
{ role: 'admin' },
{ role: 'manager' },
{ role: 'designer' },
]
})
(await user1.getProjects()).map(x => x.UserProject?.role) // [ 'admin', 'manager', 'designer' ]The BelongsToMany association automatically creates the inverse association on the target model, which is also a BelongsToMany association.
You can customize the inverse association by using the inverse option:
import { Model, InferAttributes, InferCreationAttributes, NonAttribute } from '@sequelize/core';
import { BelongsToMany } from '@sequelize/core/decorators-legacy';
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
through: 'LikedToot',
inverse: {
as: 'likers',
},
})
declare likedToots?: NonAttribute<Toot[]>;
}
class Toot extends Model<InferAttributes<Toot>, InferCreationAttributes<Toot>> {
/** Declared by {@link Person.likedToots} */
declare likers?: NonAttribute<Person[]>;
}The above would result in the following model configuration:
erDiagram
Person }o--o{ Toot : "⬇️ likedToots / ⬆️ likers"
As explained in previous sections, Many-To-Many relationships are implemented as multiple One-To-Many relationships and a junction table.
In Sequelize, the BelongsToMany association creates four associations:
- 1️⃣ One HasMany association going from the Source Model to the Through Model.
- 2️⃣ One BelongsTo association going from the Through Model to the Source Model.
- 3️⃣ One HasMany association going from the Target Model to the Through Model.
- 4️⃣ One BelongsTo association going from the Through Model to the Target Model.
erDiagram
Person }o--|| LikedToot : "⬇️ 1️⃣ likedTootsLikers / ⬆️ 2️⃣ liker"
LikedToot ||--o{ Toot : " ⬇️ 3️⃣ likedToot / ⬆️ 4️⃣ likersLikedToots"
Person }o--o{ Toot : "⬇️ likedToots / ⬆️ likers"
Their names are automatically generated based on the name of the BelongsToMany association, and the name of its inverse association.
You can customize the names of these associations by using the throughAssociations options:
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
through: 'LikedToot',
inverse: {
as: 'likers',
},
// highlight-start
throughAssociations: {
// 1️⃣ The name of the association going from the source model (Person)
// to the through model (LikedToot)
fromSource: 'likedTootsLikers',
// 2️⃣ The name of the association going from the through model (LikedToot)
// to the source model (Person)
toSource: 'liker',
// 3️⃣ The name of the association going from the target model (Toot)
// to the through model (LikedToot)
fromTarget: 'likersLikedToots',
// 4️⃣ The name of the association going from the through model (LikedToot)
// to the target model (Toot)
toTarget: 'likedToot',
},
// highlight-end
})
declare likedToots?: NonAttribute<Toot[]>;
}Sequelize will generate foreign keys automatically based on the names of your associations. It is the name of your association + the name of the attribute the association is pointing to (which defaults to the primary key).
In the example above, the foreign keys would be likerId and likedTootId, because the associations are called likedToots and likers,
and the primary keys referenced by the foreign keys are both called id.
You can customize the foreign keys by using the foreignKey and otherKey options. The foreignKey option is the foreign key that
points to the source model, and the otherKey is the foreign key that points to the target model.
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
through: 'LikedToot',
inverse: {
as: 'likers',
},
// highlight-start
// This foreign key points to the Person model
foreignKey: 'personId',
// This foreign key points to the Toot model
otherKey: 'tootId',
// highlight-end
})
declare likedToots?: NonAttribute<Toot[]>;
}By default, Sequelize will use the primary key of the source & target models as the attribute the foreign key references.
You can customize this by using the sourceKey & targetKey option.
The sourceKey option is the attribute from the model on which the association is defined,
and the targetKey is the attribute from the target model.
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
through: 'LikedToot',
inverse: {
as: 'likers',
},
// highlight-start
// The foreignKey will reference the 'id' attribute of the Person model
sourceKey: 'id',
// The otherKey will reference the 'id' attribute of the Toot model
targetKey: 'id',
// highlight-end
})
declare likedToots?: NonAttribute<Toot[]>;
}The BelongsToMany association creates a unique key on the foreign keys of the through model.
This unique key name can be changed using the through.unique option. You can also set it to false to disable the unique constraint altogether.
class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@BelongsToMany(() => Toot, {
through: {
model: 'LikedToot',
// highlight-next-line
unique: false,
},
})
declare likedToots?: NonAttribute<Toot[]>;
}All associations add methods to the source model1. These methods can be used to fetch, create, and delete associated models.
If you use TypeScript, you will need to declare these methods on your model class.
The association getter is used to fetch the associated models. It is always named get<AssociationName>:
import { BelongsToManyGetAssociationsMixin } from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare getBooks: BelongsToManyGetAssociationsMixin<Book>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
// highlight-start
const books: Book[] = await author.getBooks();
// highlight-endThe association setter is used to set the associated models. It is always named set<AssociationName>.
If the model is already associated to one or more models, the old associations are removed before the new ones are added.
import { BelongsToManySetAssociationsMixin } from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare setBooks: BelongsToManySetAssociationsMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
const [book1, book2, book3] = await Book.findAll({ limit: 3 });
// highlight-start
// Remove all previous associations and set the new ones
await author.setBooks([book1, book2, book3]);
// You can also use the primary key of the newly associated model as a way to identify it
// without having to fetch it first.
await author.setBooks([1, 2, 3]);
// highlight-endThe association adder is used to add one or more new associated models without removing existing ones. There are two versions of this method:
add<SingularAssociationName>: Associates a single new model.add<PluralAssociationName>: Associates multiple new models.
import {
BelongsToManyAddAssociationMixin,
BelongsToManyAddAssociationsMixin,
} from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare addBook: BelongsToManyAddAssociationMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
declare addBooks: BelongsToManyAddAssociationsMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
const [book1, book2, book3] = await Book.findAll({ limit: 3 });
// highlight-start
// Add a single book, without removing existing ones
await author.addBook(book1);
// Add multiple books, without removing existing ones
await author.addBooks([book1, book2]);
// You can also use the primary key of the newly associated model as a way to identify it
// without having to fetch it first.
await author.addBook(1);
await author.addBooks([1, 2, 3]);
// highlight-endThe association remover is used to remove one or more associated models.
There are two versions of this method:
remove<SingularAssociationName>: Removes a single associated model.remove<PluralAssociationName>: Removes multiple associated models.
import {
BelongsToManyRemoveAssociationMixin,
BelongsToManyRemoveAssociationsMixin,
} from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare removeBook: BelongsToManyRemoveAssociationMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
declare removeBooks: BelongsToManyRemoveAssociationsMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
const [book1, book2, book3] = await Book.findAll({ limit: 3 });
// highlight-start
// Remove a single book, without removing existing ones
await author.removeBook(book1);
// Remove multiple books, without removing existing ones
await author.removeBooks([book1, book2]);
// You can also use the primary key of the newly associated model as a way to identify it
// without having to fetch it first.
await author.removeBook(1);
await author.removeBooks([1, 2, 3]);
// highlight-endThe association creator is used to create a new associated model and associate it with the source model. It is always named create<AssociationName>.
import { BelongsToManyCreateAssociationMixin } from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare createBook: BelongsToManyCreateAssociationMixin<Book, 'postId'>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
// highlight-start
const book = await author.createBook({
content: 'This is a book',
});
// highlight-end:::info Omitting the foreign key
In the example above, we did not need to specify the postId attribute. This is because Sequelize will automatically add it to the creation attributes.
If you use TypeScript, you need to let TypeScript know that the foreign key is not required. You can do so using the second generic argument of the BelongsToManyCreateAssociationMixin type.
BelongsToManyCreateAssociationMixin<Book, 'postId'> ^ Here;:::
The association checker is used to check if a model is associated with another model. It has two versions:
has<SingularAssociationName>: Checks if a single model is associated.has<PluralAssociationName>: Checks whether all the specified models are associated.
import {
BelongsToManyHasAssociationMixin,
BelongsToManyHasAssociationsMixin,
} from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare hasBook: BelongsToManyHasAssociationMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
declare hasBooks: BelongsToManyHasAssociationsMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
// highlight-start
// Returns true if the post has a book with id 1
const isAssociated = await author.hasBook(book1);
// Returns true if the post is associated to all specified books
const isAssociated = await author.hasBooks([book1, book2, book3]);
// Like other association methods, you can also use the primary key of the associated model as a way to identify it
const isAssociated = await author.hasBooks([1, 2, 3]);
// highlight-endThe association counter is used to count the number of associated models. It is always named count<AssociationName>.
import { BelongsToManyCountAssociationsMixin } from '@sequelize/core';
class Author extends Model<InferAttributes<Author>, InferCreationAttributes<Author>> {
@BelongsToMany(() => Book, { through: 'BookAuthor' })
declare books?: NonAttribute<Book[]>;
// highlight-start
declare countBooks: BelongsToManyCountAssociationsMixin<Book>;
// highlight-end
}
// ...
const author = await Author.findByPk(1);
// highlight-start
// Returns the number of associated books
const count = await author.countBooks();
// highlight-endFootnotes
-
The source model is the model that defines the association. ↩