The BelongsToMany Association
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.
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.
The junction table is used to store the foreign keys of the two associated models.
Defining the Association
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>> {
@BelongsToMany(() => Toot, {
through: 'LikedToot',
})
declare likedToots?: NonAttribute<Toot[]>;
}
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
.
through
optionThe 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.
through: 'liked_toots',
})
declare likedToots?: NonAttribute<Toot[]>;
}
Customizing the Junction Table
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.
Inverse Association
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:
Intermediary associations
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.
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',
},
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',
},
})
declare likedToots?: NonAttribute<Toot[]>;
}
Foreign Keys Names
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',
},
// This foreign key points to the Person model
foreignKey: 'personId',
// This foreign key points to the Toot model
otherKey: 'tootId',
})
declare likedToots?: NonAttribute<Toot[]>;
}
Foreign Key targets (sourceKey
, targetKey
)
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',
},
// 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',
})
declare likedToots?: NonAttribute<Toot[]>;
}
Through Pair Unique Constraint
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',
unique: false,
},
})
declare likedToots?: NonAttribute<Toot[]>;
}
Association Methods
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.
Association Getter (getX
)
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[]>;
declare getBooks: BelongsToManyGetAssociationsMixin<Book>;
}
// ...
const author = await Author.findByPk(1);
const books: Book[] = await author.getBooks();
Association Setter (setX
)
The 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[]>;
declare setBooks: BelongsToManySetAssociationsMixin<
Book,
/* this is the type of the primary key of the target */
Book['id']
>;
}
// ...
const author = await Author.findByPk(1);
const [book1, book2, book3] = await Book.findAll({ limit: 3 });
// 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]);
Association Adder (addX
)
The 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[]>;
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']
>;
}
// ...
const author = await Author.findByPk(1);
const [book1, book2, book3] = await Book.findAll({ limit: 3 });
// 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]);
Association Remover (removeX
)
The 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[]>;
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']
>;
}
// ...
const author = await Author.findByPk(1);
const [book1, book2, book3] = await Book.findAll({ limit: 3 });
// 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]);
Association Creator (createX
)
The 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[]>;
declare createBook: BelongsToManyCreateAssociationMixin<Book, 'postId'>;
}
// ...
const author = await Author.findByPk(1);
const book = await author.createBook({
content: 'This is a book',
});
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;
Association Checker (hasX
)
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[]>;
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']
>;
}
// ...
const author = await Author.findByPk(1);
// 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]);
Association Counter (countX
)
The 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[]>;
declare countBooks: BelongsToManyCountAssociationsMixin<Book>;
}
// ...
const author = await Author.findByPk(1);
// Returns the number of associated books
const count = await author.countBooks();
Footnotes
-
The source model is the model that defines the association. ↩