Skip to main content
Version: v7 - alpha

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.

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.
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',
});
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;

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

  1. The source model is the model that defines the association.