Skip to content

Latest commit

 

History

History
608 lines (459 loc) · 18.9 KB

File metadata and controls

608 lines (459 loc) · 18.9 KB
sidebar_position 5
title BelongsToMany

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.

erDiagram
  people }o--o{ toots : likedToots
Loading

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
Loading

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>> {
  // 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[]>;
}

:::

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.

Associations with extra attributes on through table

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' ]

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:

erDiagram
  Person }o--o{ Toot : "⬇️ likedToots / ⬆️ likers"
Loading

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.
erDiagram
  Person }o--|| LikedToot : "⬇️ 1️⃣ likedTootsLikers / ⬆️ 2️⃣ liker"
  LikedToot ||--o{ Toot : " ⬇️ 3️⃣ likedToot / ⬆️ 4️⃣ likersLikedToots"
  Person }o--o{ Toot : "⬇️ likedToots / ⬆️ likers"
Loading

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[]>;
}

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',
    },
    // 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[]>;
}

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',
    },
    // 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[]>;
}

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',
      // highlight-next-line
      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[]>;

  // highlight-start
  declare getBooks: BelongsToManyGetAssociationsMixin<Book>;
  // highlight-end
}

// ...

const author = await Author.findByPk(1);

// highlight-start
const books: Book[] = await author.getBooks();
// highlight-end

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[]>;

  // 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-end

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[]>;

  // 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-end

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[]>;

  // 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-end

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[]>;

  // 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;

:::

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[]>;

  // 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-end

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[]>;

  // 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-end

Footnotes

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