diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..a86b051e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @sequelize/code-reviewers diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1bad4b3d..c30a9c2c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: sequelize patreon: # Replace with a single Patreon username open_collective: sequelize ko_fi: # Replace with a single Ko-fi username diff --git a/.github/workflows/autoupdate.yml b/.github/workflows/autoupdate.yml new file mode 100644 index 00000000..841aee7a --- /dev/null +++ b/.github/workflows/autoupdate.yml @@ -0,0 +1,40 @@ +name: auto-update PRs & label conflicts +on: + push: + branches: + - main + # can also be used with the pull_request event + pull_request_target: + types: + - synchronize + # allow the workflow to correct any label incorrectly added or removed by a user. + - labeled + - unlabeled + # update the PR as soon as the auto-merge is enabled. + - auto_merge_enabled + # process the PR once they appear in the search filters + - ready_for_review + - opened + - reopened +jobs: + autoupdate: + runs-on: ubuntu-latest + steps: + - name: Generate Sequelize Bot Token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: '${{ secrets.SEQUELIZE_BOT_APP_ID }}' + private-key: '${{ secrets.SEQUELIZE_BOT_PRIVATE_KEY }}' + - uses: sequelize/pr-auto-update-and-handle-conflicts@257ac5f68859672393e3320495164251140bd801 # 1.0.1 + with: + conflict-label: 'conflicted' + conflict-requires-ready-state: 'ready_for_review' + conflict-excluded-authors: 'bot/renovate' + update-pr-branches: true + update-requires-auto-merge: true + update-requires-ready-state: 'ready_for_review' + update-excluded-authors: 'bot/renovate' + update-excluded-labels: 'no-autoupdate' + env: + GITHUB_TOKEN: '${{ steps.generate-token.outputs.token }}' diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index 2924d57c..f073abce 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -14,13 +14,14 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 20.x cache: yarn - run: yarn install --frozen-lockfile - run: yarn lint-no-fix + - run: yarn lint-scss-no-fix - run: yarn typecheck - run: yarn sync - run: yarn build @@ -33,7 +34,7 @@ jobs: shell: bash run: | netlify deploy --dir build --message '${{ github.event.commits[0].message }}' > log.tmp.txt 2>&1 - cat log.tmp.txt | grep -E 'Logs|Website Draft URL' > log.txt + cat log.tmp.txt | grep -E 'Website draft URL:' > log.txt - name: Read deployment log if: '! github.event.pull_request.head.repo.fork' id: logs @@ -43,7 +44,7 @@ jobs: - name: Comment PR with draft publish logs if: '! github.event.pull_request.head.repo.fork' id: create-comment - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.pull_request.number }} body: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 971b30c4..1d733d24 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,10 +12,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 20.x cache: yarn - run: yarn install --frozen-lockfile - run: yarn lint-no-fix diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af2198..2312dc58 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/.prettierrc.json b/.prettierrc.json index 8db60caa..544138be 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,3 @@ { - "singleQuote": true + "singleQuote": true } diff --git a/docs/_fragments/_decorator-info.mdx b/docs/_fragments/_decorator-info.mdx new file mode 100644 index 00000000..7f47d8c4 --- /dev/null +++ b/docs/_fragments/_decorator-info.mdx @@ -0,0 +1,16 @@ +:::info + +Sequelize currently only supports the [_legacy/experimental decorator format_](https://github.com/tc39/proposal-decorators#how-does-this-proposal-compare-to-other-versions-of-decorators). +Support for the [_new decorator format_](https://github.com/tc39/proposal-decorators) will be added in a future release. + +All decorators must be imported from `@sequelize/core/decorators-legacy`: + +```ts +import { Attribute, Table } from '@sequelize/core/decorators-legacy'; +``` + +Using legacy decorators requires to use a transpiler such as [TypeScript](https://www.typescriptlang.org/docs/handbook/decorators.html#decorators), +[Babel](https://babeljs.io/docs/babel-plugin-proposal-decorators) or others to compile them to JavaScript. +Alternatively, Sequelize also supports [a legacy approach](../other-topics/legacy-model-definitions.mdx) that does not require using decorators, but this is discouraged. + +::: diff --git a/docs/_fragments/_js-default-caution.mdx b/docs/_fragments/_js-default-caution.mdx new file mode 100644 index 00000000..20abee07 --- /dev/null +++ b/docs/_fragments/_js-default-caution.mdx @@ -0,0 +1,9 @@ +:::caution + +The generation of values for `DataTypes.NOW` and other JavaScript functions are not handled by the Database, +but by Sequelize itself. This means that they will only be used when using Model methods. They will not be used in [raw queries](../querying/raw-queries.mdx), +in [migrations](../models/migrations.md), and all other places where Sequelize does not have access to the Model. + +Read about SQL based alternatives in [Dynamic SQL default values](../models/defining-models.mdx#dynamic-sql-default-values). + +::: diff --git a/docs/_fragments/_uuid-support-table.mdx b/docs/_fragments/_uuid-support-table.mdx new file mode 100644 index 00000000..809b31ff --- /dev/null +++ b/docs/_fragments/_uuid-support-table.mdx @@ -0,0 +1,10 @@ +import { DialectTableFilter } from '@site/src/components/dialect-table-filter.tsx'; + + + +| | PostgreSQL | MariaDB | MySQL | MSSQL | SQLite | Snowflake | db2 | ibmi | +|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|--------|-----------|-----|------| +| `uuidV1` | [`uuid_generate_v1`](https://www.postgresql.org/docs/current/uuid-ossp.html) (requires `uuid-ossp`) | [`UUID`](https://mariadb.com/kb/en/uuid/) | [`UUID`](https://dev.mysql.com/doc/refman/8.0/en/miscellaneous-functions.html#function_uuid) | N/A | N/A | N/A | N/A | N/A | +| `uuidV4` | __pg >= v13__: [`gen_random_uuid`](https://www.postgresql.org/docs/current/functions-uuid.html)
__pg < v13__: [`uuid_generate_v4`](https://www.postgresql.org/docs/current/uuid-ossp.html) (requires `uuid-ossp`) | N/A | N/A | [`NEWID`](https://learn.microsoft.com/en-us/sql/t-sql/functions/newid-transact-sql?view=sql-server-ver16) | N/A | N/A | N/A | N/A | + +
diff --git a/docs/advanced-association-concepts/advanced-many-to-many.md b/docs/advanced-association-concepts/advanced-many-to-many.md deleted file mode 100644 index 4ad22b1e..00000000 --- a/docs/advanced-association-concepts/advanced-many-to-many.md +++ /dev/null @@ -1,670 +0,0 @@ ---- -sidebar_position: 3 -title: Advanced M:N Associations ---- - -Make sure you have read the [associations guide](../core-concepts/assocs.md) before reading this guide. - -Let's start with an example of a Many-to-Many relationship between `User` and `Profile`. - -```js -const User = sequelize.define('user', { - username: DataTypes.STRING, - points: DataTypes.INTEGER -}, { timestamps: false }); -const Profile = sequelize.define('profile', { - name: DataTypes.STRING -}, { timestamps: false }); -``` - -The simplest way to define the Many-to-Many relationship is: - -```js -User.belongsToMany(Profile, { through: 'User_Profiles' }); -Profile.belongsToMany(User, { through: 'User_Profiles' }); -``` - -By passing a string to `through` above, we are asking Sequelize to automatically generate a model named `User_Profiles` as the *through table* (also known as junction table), with only two columns: `userId` and `profileId`. A composite unique key will be established on these two columns. - -We can also define ourselves a model to be used as the through table. - -```js -const User_Profile = sequelize.define('User_Profile', {}, { timestamps: false }); -User.belongsToMany(Profile, { through: User_Profile }); -Profile.belongsToMany(User, { through: User_Profile }); -``` - -The above has the exact same effect. Note that we didn't define any attributes on the `User_Profile` model. The fact that we passed it into a `belongsToMany` call tells sequelize to create the two attributes `userId` and `profileId` automatically, just like other associations also cause Sequelize to automatically add a column to one of the involved models. - -However, defining the model by ourselves has several advantages. We can, for example, define more columns on our through table: - -```js -const User_Profile = sequelize.define('User_Profile', { - selfGranted: DataTypes.BOOLEAN -}, { timestamps: false }); -User.belongsToMany(Profile, { through: User_Profile }); -Profile.belongsToMany(User, { through: User_Profile }); -``` - -With this, we can now track an extra information at the through table, namely the `selfGranted` boolean. For example, when calling the `user.addProfile()` we can pass values for the extra columns using the `through` option. - -Example: - -```js -const amidala = await User.create({ username: 'p4dm3', points: 1000 }); -const queen = await Profile.create({ name: 'Queen' }); -await amidala.addProfile(queen, { through: { selfGranted: false } }); -const result = await User.findOne({ - where: { username: 'p4dm3' }, - include: Profile -}); -console.log(result); -``` - -Output: - -```json -{ - "id": 4, - "username": "p4dm3", - "points": 1000, - "profiles": [ - { - "id": 6, - "name": "queen", - "User_Profile": { - "userId": 4, - "profileId": 6, - "selfGranted": false - } - } - ] -} -``` - -You can create all relationship in single `create` call too. - -Example: - -```js -const amidala = await User.create({ - username: 'p4dm3', - points: 1000, - profiles: [{ - name: 'Queen', - User_Profile: { - selfGranted: true - } - }] -}, { - include: Profile -}); - -const result = await User.findOne({ - where: { username: 'p4dm3' }, - include: Profile -}); - -console.log(result); -``` - -Output: - -```json -{ - "id": 1, - "username": "p4dm3", - "points": 1000, - "profiles": [ - { - "id": 1, - "name": "Queen", - "User_Profile": { - "selfGranted": true, - "userId": 1, - "profileId": 1 - } - } - ] -} -``` - -You probably noticed that the `User_Profiles` table does not have an `id` field. As mentioned above, it has a composite unique key instead. The name of this composite unique key is chosen automatically by Sequelize but can be customized with the `uniqueKey` option: - -```js -User.belongsToMany(Profile, { through: User_Profiles, uniqueKey: 'my_custom_unique' }); -``` - -Another possibility, if desired, is to force the through table to have a primary key just like other standard tables. To do this, simply define the primary key in the model: - -```js -const User_Profile = sequelize.define('User_Profile', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false - }, - selfGranted: DataTypes.BOOLEAN -}, { timestamps: false }); -User.belongsToMany(Profile, { through: User_Profile }); -Profile.belongsToMany(User, { through: User_Profile }); -``` - -The above will still create two columns `userId` and `profileId`, of course, but instead of setting up a composite unique key on them, the model will use its `id` column as primary key. Everything else will still work just fine. - -## Through tables versus normal tables and the "Super Many-to-Many association" - -Now we will compare the usage of the last Many-to-Many setup shown above with the usual One-to-Many relationships, so that in the end we conclude with the concept of a *"Super Many-to-Many relationship"*. - -### Models recap (with minor rename) - -To make things easier to follow, let's rename our `User_Profile` model to `grant`. Note that everything works in the same way as before. Our models are: - -```js -const User = sequelize.define('user', { - username: DataTypes.STRING, - points: DataTypes.INTEGER -}, { timestamps: false }); - -const Profile = sequelize.define('profile', { - name: DataTypes.STRING -}, { timestamps: false }); - -const Grant = sequelize.define('grant', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false - }, - selfGranted: DataTypes.BOOLEAN -}, { timestamps: false }); -``` - -We established a Many-to-Many relationship between `User` and `Profile` using the `Grant` model as the through table: - -```js -User.belongsToMany(Profile, { through: Grant }); -Profile.belongsToMany(User, { through: Grant }); -``` - -This automatically added the columns `userId` and `profileId` to the `Grant` model. - -**Note:** As shown above, we have chosen to force the `grant` model to have a single primary key (called `id`, as usual). This is necessary for the *Super Many-to-Many relationship* that will be defined soon. - -### Using One-to-Many relationships instead - -Instead of setting up the Many-to-Many relationship defined above, what if we did the following instead? - -```js -// Setup a One-to-Many relationship between User and Grant -User.hasMany(Grant); -Grant.belongsTo(User); - -// Also setup a One-to-Many relationship between Profile and Grant -Profile.hasMany(Grant); -Grant.belongsTo(Profile); -``` - -The result is essentially the same! This is because `User.hasMany(Grant)` and `Profile.hasMany(Grant)` will automatically add the `userId` and `profileId` columns to `Grant`, respectively. - -This shows that one Many-to-Many relationship isn't very different from two One-to-Many relationships. The tables in the database look the same. - -The only difference is when you try to perform an eager load with Sequelize. - -```js -// With the Many-to-Many approach, you can do: -User.findAll({ include: Profile }); -Profile.findAll({ include: User }); -// However, you can't do: -User.findAll({ include: Grant }); -Profile.findAll({ include: Grant }); -Grant.findAll({ include: User }); -Grant.findAll({ include: Profile }); - -// On the other hand, with the double One-to-Many approach, you can do: -User.findAll({ include: Grant }); -Profile.findAll({ include: Grant }); -Grant.findAll({ include: User }); -Grant.findAll({ include: Profile }); -// However, you can't do: -User.findAll({ include: Profile }); -Profile.findAll({ include: User }); -// Although you can emulate those with nested includes, as follows: -User.findAll({ - include: { - model: Grant, - include: Profile - } -}); // This emulates the `User.findAll({ include: Profile })`, however - // the resulting object structure is a bit different. The original - // structure has the form `user.profiles[].grant`, while the emulated - // structure has the form `user.grants[].profiles[]`. -``` - -### The best of both worlds: the Super Many-to-Many relationship - -We can simply combine both approaches shown above! - -```js -// The Super Many-to-Many relationship -User.belongsToMany(Profile, { through: Grant }); -Profile.belongsToMany(User, { through: Grant }); -User.hasMany(Grant); -Grant.belongsTo(User); -Profile.hasMany(Grant); -Grant.belongsTo(Profile); -``` - -This way, we can do all kinds of eager loading: - -```js -// All these work: -User.findAll({ include: Profile }); -Profile.findAll({ include: User }); -User.findAll({ include: Grant }); -Profile.findAll({ include: Grant }); -Grant.findAll({ include: User }); -Grant.findAll({ include: Profile }); -``` - -We can even perform all kinds of deeply nested includes: - -```js -User.findAll({ - include: [ - { - model: Grant, - include: [User, Profile] - }, - { - model: Profile, - include: { - model: User, - include: { - model: Grant, - include: [User, Profile] - } - } - } - ] -}); -``` - -## Aliases and custom key names - -Similarly to the other relationships, aliases can be defined for Many-to-Many relationships. - -Before proceeding, please recall [the aliasing example for `belongsTo`](../core-concepts/assocs.md#defining-an-alias) on the [associations guide](../core-concepts/assocs.md). Note that, in that case, defining an association impacts both the way includes are done (i.e. passing the association name) and the name Sequelize chooses for the foreign key (in that example, `leaderId` was created on the `Ship` model). - -Defining an alias for a `belongsToMany` association also impacts the way includes are performed: - -```js -Product.belongsToMany(Category, { as: 'groups', through: 'product_categories' }); -Category.belongsToMany(Product, { as: 'items', through: 'product_categories' }); - -// [...] - -await Product.findAll({ include: Category }); // This doesn't work - -await Product.findAll({ // This works, passing the alias - include: { - model: Category, - as: 'groups' - } -}); - -await Product.findAll({ include: 'groups' }); // This also works -``` - -However, defining an alias here has nothing to do with the foreign key names. The names of both foreign keys created in the through table are still constructed by Sequelize based on the name of the models being associated. This can readily be seen by inspecting the generated SQL for the through table in the example above: - -```sql -CREATE TABLE IF NOT EXISTS `product_categories` ( - `createdAt` DATETIME NOT NULL, - `updatedAt` DATETIME NOT NULL, - `productId` INTEGER NOT NULL REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - `categoryId` INTEGER NOT NULL REFERENCES `categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - PRIMARY KEY (`productId`, `categoryId`) -); -``` - -We can see that the foreign keys are `productId` and `categoryId`. To change these names, Sequelize accepts the options `foreignKey` and `otherKey` respectively (i.e., the `foreignKey` defines the key for the source model in the through relation, and `otherKey` defines it for the target model): - -```js -Product.belongsToMany(Category, { - through: 'product_categories', - foreignKey: 'objectId', // replaces `productId` - otherKey: 'typeId' // replaces `categoryId` -}); -Category.belongsToMany(Product, { - through: 'product_categories', - foreignKey: 'typeId', // replaces `categoryId` - otherKey: 'objectId' // replaces `productId` -}); -``` - -Generated SQL: - -```sql -CREATE TABLE IF NOT EXISTS `product_categories` ( - `createdAt` DATETIME NOT NULL, - `updatedAt` DATETIME NOT NULL, - `objectId` INTEGER NOT NULL REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - `typeId` INTEGER NOT NULL REFERENCES `categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - PRIMARY KEY (`objectId`, `typeId`) -); -``` - -As shown above, when you define a Many-to-Many relationship with two `belongsToMany` calls (which is the standard way), you should provide the `foreignKey` and `otherKey` options appropriately in both calls. If you pass these options in only one of the calls, the Sequelize behavior will be unreliable. - -## Self-references - -Sequelize supports self-referential Many-to-Many relationships, intuitively: - -```js -Person.belongsToMany(Person, { as: 'Children', through: 'PersonChildren' }) -// This will create the table PersonChildren which stores the ids of the objects. -``` - -## Specifying attributes from the through table - -By default, when eager loading a many-to-many relationship, Sequelize will return data in the following structure (based on the first example in this guide): - -```json -// User.findOne({ include: Profile }) -{ - "id": 4, - "username": "p4dm3", - "points": 1000, - "profiles": [ - { - "id": 6, - "name": "queen", - "grant": { - "userId": 4, - "profileId": 6, - "selfGranted": false - } - } - ] -} -``` - -Notice that the outer object is an `User`, which has a field called `profiles`, which is a `Profile` array, such that each `Profile` comes with an extra field called `grant` which is a `Grant` instance. This is the default structure created by Sequelize when eager loading from a Many-to-Many relationship. - -However, if you want only some of the attributes of the through table, you can provide an array with the attributes you want in the `attributes` option. For example, if you only want the `selfGranted` attribute from the through table: - -```js -User.findOne({ - include: { - model: Profile, - through: { - attributes: ['selfGranted'] - } - } -}); -``` - -Output: - -```json -{ - "id": 4, - "username": "p4dm3", - "points": 1000, - "profiles": [ - { - "id": 6, - "name": "queen", - "grant": { - "selfGranted": false - } - } - ] -} -``` - -If you don't want the nested `grant` field at all, use `attributes: []`: - -```js -User.findOne({ - include: { - model: Profile, - through: { - attributes: [] - } - } -}); -``` - -Output: - -```json -{ - "id": 4, - "username": "p4dm3", - "points": 1000, - "profiles": [ - { - "id": 6, - "name": "queen" - } - ] -} -``` - -If you are using mixins (such as `user.getProfiles()`) instead of finder methods (such as `User.findAll()`), you have to use the `joinTableAttributes` option instead: - -```js -someUser.getProfiles({ joinTableAttributes: ['selfGranted'] }); -``` - -Output: - -```json -[ - { - "id": 6, - "name": "queen", - "grant": { - "selfGranted": false - } - } -] -``` - -## Many-to-many-to-many relationships and beyond - -Consider you are trying to model a game championship. There are players and teams. Teams play games. However, players can change teams in the middle of the championship (but not in the middle of a game). So, given one specific game, there are certain teams participating in that game, and each of these teams has a set of players (for that game). - -So we start by defining the three relevant models: - -```js -const Player = sequelize.define('Player', { username: DataTypes.STRING }); -const Team = sequelize.define('Team', { name: DataTypes.STRING }); -const Game = sequelize.define('Game', { name: DataTypes.STRING }); -``` - -Now, the question is: how to associate them? - -First, we note that: - -* One game has many teams associated to it (the ones that are playing that game); -* One team may have participated in many games. - -The above observations show that we need a Many-to-Many relationship between Game and Team. Let's use the Super Many-to-Many relationship as explained earlier in this guide: - -```js -// Super Many-to-Many relationship between Game and Team -const GameTeam = sequelize.define('GameTeam', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false - } -}); -Team.belongsToMany(Game, { through: GameTeam }); -Game.belongsToMany(Team, { through: GameTeam }); -GameTeam.belongsTo(Game); -GameTeam.belongsTo(Team); -Game.hasMany(GameTeam); -Team.hasMany(GameTeam); -``` - -The part about players is trickier. We note that the set of players that form a team depends not only on the team (obviously), but also on which game is being considered. Therefore, we don't want a Many-to-Many relationship between Player and Team. We also don't want a Many-to-Many relationship between Player and Game. Instead of associating a Player to any of those models, what we need is an association between a Player and something like a *"team-game pair constraint"*, since it is the pair (team plus game) that defines which players belong there. So what we are looking for turns out to be precisely the junction model, GameTeam, itself! And, we note that, since a given *game-team pair* specifies many players, and on the other hand that the same player can participate of many *game-team pairs*, we need a Many-to-Many relationship between Player and GameTeam! - -To provide the greatest flexibility, let's use the Super Many-to-Many relationship construction here again: - -```js -// Super Many-to-Many relationship between Player and GameTeam -const PlayerGameTeam = sequelize.define('PlayerGameTeam', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false - } -}); -Player.belongsToMany(GameTeam, { through: PlayerGameTeam }); -GameTeam.belongsToMany(Player, { through: PlayerGameTeam }); -PlayerGameTeam.belongsTo(Player); -PlayerGameTeam.belongsTo(GameTeam); -Player.hasMany(PlayerGameTeam); -GameTeam.hasMany(PlayerGameTeam); -``` - -The above associations achieve precisely what we want. Here is a full runnable example of this: - -```js -const { Sequelize, Op, Model, DataTypes } = require('@sequelize/core'); -const sequelize = new Sequelize('sqlite::memory:', { - define: { timestamps: false } // Just for less clutter in this example -}); -const Player = sequelize.define('Player', { username: DataTypes.STRING }); -const Team = sequelize.define('Team', { name: DataTypes.STRING }); -const Game = sequelize.define('Game', { name: DataTypes.STRING }); - -// We apply a Super Many-to-Many relationship between Game and Team -const GameTeam = sequelize.define('GameTeam', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false - } -}); -Team.belongsToMany(Game, { through: GameTeam }); -Game.belongsToMany(Team, { through: GameTeam }); -GameTeam.belongsTo(Game); -GameTeam.belongsTo(Team); -Game.hasMany(GameTeam); -Team.hasMany(GameTeam); - -// We apply a Super Many-to-Many relationship between Player and GameTeam -const PlayerGameTeam = sequelize.define('PlayerGameTeam', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false - } -}); -Player.belongsToMany(GameTeam, { through: PlayerGameTeam }); -GameTeam.belongsToMany(Player, { through: PlayerGameTeam }); -PlayerGameTeam.belongsTo(Player); -PlayerGameTeam.belongsTo(GameTeam); -Player.hasMany(PlayerGameTeam); -GameTeam.hasMany(PlayerGameTeam); - -(async () => { - - await sequelize.sync(); - await Player.bulkCreate([ - { username: 's0me0ne' }, - { username: 'empty' }, - { username: 'greenhead' }, - { username: 'not_spock' }, - { username: 'bowl_of_petunias' } - ]); - await Game.bulkCreate([ - { name: 'The Big Clash' }, - { name: 'Winter Showdown' }, - { name: 'Summer Beatdown' } - ]); - await Team.bulkCreate([ - { name: 'The Martians' }, - { name: 'The Earthlings' }, - { name: 'The Plutonians' } - ]); - - // Let's start defining which teams were in which games. This can be done - // in several ways, such as calling `.setTeams` on each game. However, for - // brevity, we will use direct `create` calls instead, referring directly - // to the IDs we want. We know that IDs are given in order starting from 1. - await GameTeam.bulkCreate([ - { GameId: 1, TeamId: 1 }, // this GameTeam will get id 1 - { GameId: 1, TeamId: 2 }, // this GameTeam will get id 2 - { GameId: 2, TeamId: 1 }, // this GameTeam will get id 3 - { GameId: 2, TeamId: 3 }, // this GameTeam will get id 4 - { GameId: 3, TeamId: 2 }, // this GameTeam will get id 5 - { GameId: 3, TeamId: 3 } // this GameTeam will get id 6 - ]); - - // Now let's specify players. - // For brevity, let's do it only for the second game (Winter Showdown). - // Let's say that that s0me0ne and greenhead played for The Martians, while - // not_spock and bowl_of_petunias played for The Plutonians: - await PlayerGameTeam.bulkCreate([ - // In 'Winter Showdown' (i.e. GameTeamIds 3 and 4): - { PlayerId: 1, GameTeamId: 3 }, // s0me0ne played for The Martians - { PlayerId: 3, GameTeamId: 3 }, // greenhead played for The Martians - { PlayerId: 4, GameTeamId: 4 }, // not_spock played for The Plutonians - { PlayerId: 5, GameTeamId: 4 } // bowl_of_petunias played for The Plutonians - ]); - - // Now we can make queries! - const game = await Game.findOne({ - where: { - name: "Winter Showdown" - }, - include: { - model: GameTeam, - include: [ - { - model: Player, - through: { attributes: [] } // Hide unwanted `PlayerGameTeam` nested object from results - }, - Team - ] - } - }); - - console.log(`Found game: "${game.name}"`); - for (let i = 0; i < game.GameTeams.length; i++) { - const team = game.GameTeams[i].Team; - const players = game.GameTeams[i].Players; - console.log(`- Team "${team.name}" played game "${game.name}" with the following players:`); - console.log(players.map(p => `--- ${p.username}`).join('\n')); - } - -})(); -``` - -Output: - -```text -Found game: "Winter Showdown" -- Team "The Martians" played game "Winter Showdown" with the following players: ---- s0me0ne ---- greenhead -- Team "The Plutonians" played game "Winter Showdown" with the following players: ---- not_spock ---- bowl_of_petunias -``` - -So this is how we can achieve a *many-to-many-to-many* relationship between three models in Sequelize, by taking advantage of the Super Many-to-Many relationship technique! - -This idea can be applied recursively for even more complex, *many-to-many-to-...-to-many* relationships (although at some point queries might become slow). diff --git a/docs/advanced-association-concepts/association-scopes.md b/docs/advanced-association-concepts/association-scopes.md deleted file mode 100644 index 06a147e3..00000000 --- a/docs/advanced-association-concepts/association-scopes.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -sidebar_position: 4 -title: Association Scopes ---- - -This section concerns association scopes, which are similar but not the same as [model scopes](../other-topics/scopes.md). - -Association scopes can be placed both on the associated model (the target of the association) and on the through table for Many-to-Many relationships. - -## Concept - -Similarly to how a [model scope](../other-topics/scopes.md) is automatically applied on the model static calls, such as `Model.scope('foo').findAll()`, an association scope is a rule (more precisely, a set of default attributes and options) that is automatically applied on instance calls from the model. Here, *instance calls* mean method calls that are called from an instance (rather than from the Model itself). Mixins are the main example of instance methods (`instance.getSomething`, `instance.setSomething`, `instance.addSomething` and `instance.createSomething`). - -Association scopes behave just like model scopes, in the sense that both cause an automatic application of things like `where` clauses to finder calls; the difference being that instead of applying to static finder calls (which is the case for model scopes), the association scopes automatically apply to instance finder calls (such as mixins). - -## Example - -A basic example of an association scope for the One-to-Many association between models `Foo` and `Bar` is shown below. - -* Setup: - - ```js - const Foo = sequelize.define('foo', { name: DataTypes.STRING }); - const Bar = sequelize.define('bar', { status: DataTypes.STRING }); - Foo.hasMany(Bar, { - scope: { - status: 'open' - }, - as: 'openBars' - }); - await sequelize.sync(); - const myFoo = await Foo.create({ name: "My Foo" }); - ``` - -* After this setup, calling `myFoo.getOpenBars()` generates the following SQL: - - ```sql - SELECT - `id`, `status`, `createdAt`, `updatedAt`, `fooId` - FROM `bars` AS `bar` - WHERE `bar`.`status` = 'open' AND `bar`.`fooId` = 1; - ``` - -With this we can see that upon calling the `.getOpenBars()` mixin, the association scope `{ status: 'open' }` was automatically applied into the `WHERE` clause of the generated SQL. - -## Achieving the same behavior with standard scopes - -We could have achieved the same behavior with standard scopes: - -```js -// Foo.hasMany(Bar, { -// scope: { -// status: 'open' -// }, -// as: 'openBars' -// }); - -Bar.addScope('open', { - where: { - status: 'open' - } -}); -Foo.hasMany(Bar); -Foo.hasMany(Bar.scope('open'), { as: 'openBars' }); -``` - -With the above code, `myFoo.getOpenBars()` yields the same SQL shown above. diff --git a/docs/advanced-association-concepts/creating-with-associations.md b/docs/advanced-association-concepts/creating-with-associations.md deleted file mode 100644 index e152e485..00000000 --- a/docs/advanced-association-concepts/creating-with-associations.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -sidebar_position: 2 -title: Creating with Associations ---- - -An instance can be created with nested association in one step, provided all elements are new. - -In contrast, performing updates and deletions involving nested objects is currently not possible. For that, you will have to perform each separate action explicitly. - -## BelongsTo / HasMany / HasOne association - -Consider the following models: - -```js -class Product extends Model {} -Product.init({ - title: DataTypes.STRING -}, { sequelize, modelName: 'product' }); -class User extends Model {} -User.init({ - firstName: DataTypes.STRING, - lastName: DataTypes.STRING -}, { sequelize, modelName: 'user' }); -class Address extends Model {} -Address.init({ - type: DataTypes.STRING, - line1: DataTypes.STRING, - line2: DataTypes.STRING, - city: DataTypes.STRING, - state: DataTypes.STRING, - zip: DataTypes.STRING, -}, { sequelize, modelName: 'address' }); - -// We save the return values of the association setup calls to use them later -Product.User = Product.belongsTo(User); -User.Addresses = User.hasMany(Address); -// Also works for `hasOne` -``` - -A new `Product`, `User`, and one or more `Address` can be created in one step in the following way: - -```js -return Product.create({ - title: 'Chair', - user: { - firstName: 'Mick', - lastName: 'Broadstone', - addresses: [{ - type: 'home', - line1: '100 Main St.', - city: 'Austin', - state: 'TX', - zip: '78704' - }] - } -}, { - include: [{ - association: Product.User, - include: [ User.Addresses ] - }] -}); -``` - -Observe the usage of the `include` option in the `Product.create` call. That is necessary for Sequelize to understand what you are trying to create along with the association. - -Note: here, our user model is called `user`, with a lowercase `u` - This means that the property in the object should also be `user`. If the name given to `sequelize.define` was `User`, the key in the object should also be `User`. Likewise for `addresses`, except it's pluralized being a `hasMany` association. - -## BelongsTo association with an alias - -The previous example can be extended to support an association alias. - -```js -const Creator = Product.belongsTo(User, { as: 'creator' }); - -return Product.create({ - title: 'Chair', - creator: { - firstName: 'Matt', - lastName: 'Hansen' - } -}, { - include: [ Creator ] -}); -``` - -## HasMany / BelongsToMany association - -Let's introduce the ability to associate a product with many tags. Setting up the models could look like: - -```js -class Tag extends Model {} -Tag.init({ - name: DataTypes.STRING -}, { sequelize, modelName: 'tag' }); - -Product.hasMany(Tag); -// Also works for `belongsToMany`. -``` - -Now we can create a product with multiple tags in the following way: - -```js -Product.create({ - id: 1, - title: 'Chair', - tags: [ - { name: 'Alpha'}, - { name: 'Beta'} - ] -}, { - include: [ Tag ] -}) -``` - -And, we can modify this example to support an alias as well: - -```js -const Categories = Product.hasMany(Tag, { as: 'categories' }); - -Product.create({ - id: 1, - title: 'Chair', - categories: [ - { id: 1, name: 'Alpha' }, - { id: 2, name: 'Beta' } - ] -}, { - include: [{ - association: Categories, - as: 'categories' - }] -}) -``` diff --git a/docs/advanced-association-concepts/eager-loading.md b/docs/advanced-association-concepts/eager-loading.md deleted file mode 100644 index 0cfabd3a..00000000 --- a/docs/advanced-association-concepts/eager-loading.md +++ /dev/null @@ -1,669 +0,0 @@ ---- -sidebar_position: 1 -title: Eager Loading ---- - -As briefly mentioned in [the associations guide](../core-concepts/assocs.md), eager Loading is the act of querying data of several models at once (one 'main' model and one or more associated models). At the SQL level, this is a query with one or more [joins](https://en.wikipedia.org/wiki/Join_\(SQL\)). - -When this is done, the associated models will be added by Sequelize in appropriately named, automatically created field(s) in the returned objects. - -In Sequelize, eager loading is mainly done by using the `include` option on a model finder query (such as `findOne`, `findAll`, etc). - -## Basic example - -Let's assume the following setup: - -```js -const User = sequelize.define('user', { name: DataTypes.STRING }, { timestamps: false }); -const Task = sequelize.define('task', { name: DataTypes.STRING }, { timestamps: false }); -const Tool = sequelize.define('tool', { - name: DataTypes.STRING, - size: DataTypes.STRING -}, { timestamps: false }); -User.hasMany(Task); -Task.belongsTo(User); -User.hasMany(Tool, { as: 'Instruments' }); -``` - -### Fetching a single associated element - -OK. So, first of all, let's load all tasks with their associated user: - -```js -const tasks = await Task.findAll({ include: User }); -console.log(JSON.stringify(tasks, null, 2)); -``` - -Output: - -```json -[{ - "name": "A Task", - "id": 1, - "userId": 1, - "user": { - "name": "John Doe", - "id": 1 - } -}] -``` - -Here, `tasks[0].user instanceof User` is `true`. This shows that when Sequelize fetches associated models, they are added to the output object as model instances. - -Above, the associated model was added to a new field called `user` in the fetched task. The name of this field was automatically chosen by Sequelize based on the name of the associated model, where its pluralized form is used when applicable (i.e., when the association is `hasMany` or `belongsToMany`). In other words, since `Task.belongsTo(User)`, a task is associated to one user, therefore the logical choice is the singular form (which Sequelize follows automatically). - -### Fetching all associated elements - -Now, instead of loading the user that is associated to a given task, we will do the opposite - we will find all tasks associated to a given user. - -The method call is essentially the same. The only difference is that now the extra field created in the query result uses the pluralized form (`tasks` in this case), and its value is an array of task instances (instead of a single instance, as above). - -```js -const users = await User.findAll({ include: Task }); -console.log(JSON.stringify(users, null, 2)); -``` - -Output: - -```json -[{ - "name": "John Doe", - "id": 1, - "tasks": [{ - "name": "A Task", - "id": 1, - "userId": 1 - }] -}] -``` - -Notice that the accessor (the `tasks` property in the resulting instance) is pluralized since the association is one-to-many. - -### Fetching an Aliased association - -If an association is aliased (using the `as` option), you must specify this alias when including the model. Instead of passing the model directly to the `include` option, you should instead provide an object with two options: `model` and `as`. - -Notice how the user's `Tool`s are aliased as `Instruments` above. In order to get that right you have to specify the model you want to load, as well as the alias: - -```js -const users = await User.findAll({ - include: { model: Tool, as: 'Instruments' } -}); -console.log(JSON.stringify(users, null, 2)); -``` - -Output: - -```json -[{ - "name": "John Doe", - "id": 1, - "Instruments": [{ - "name": "Scissor", - "id": 1, - "userId": 1 - }] -}] -``` - -You can also include by alias name by specifying a string that matches the association alias: - -```js -User.findAll({ include: 'Instruments' }); // Also works -User.findAll({ include: { association: 'Instruments' } }); // Also works -``` - -### Required eager loading - -When eager loading, we can force the query to return only records which have an associated model, effectively converting the query from the default `OUTER JOIN` to an `INNER JOIN`. This is done with the `required: true` option, as follows: - -```js -User.findAll({ - include: { - model: Task, - required: true - } -}); -``` - -This option also works on nested includes. - -### Eager loading filtered at the associated model level - -When eager loading, we can also filter the associated model using the `where` option, as in the following example: - -```js -User.findAll({ - include: { - model: Tool, - as: 'Instruments', - where: { - size: { - [Op.ne]: 'small' - } - } - } -}); -``` - -Generated SQL: - -```sql -SELECT - `user`.`id`, - `user`.`name`, - `Instruments`.`id` AS `Instruments.id`, - `Instruments`.`name` AS `Instruments.name`, - `Instruments`.`size` AS `Instruments.size`, - `Instruments`.`userId` AS `Instruments.userId` -FROM `users` AS `user` -INNER JOIN `tools` AS `Instruments` ON - `user`.`id` = `Instruments`.`userId` AND - `Instruments`.`size` != 'small'; -``` - -Note that the SQL query generated above will only fetch users that have at least one tool that matches the condition (of not being `small`, in this case). This is the case because, when the `where` option is used inside an `include`, Sequelize automatically sets the `required` option to `true`. This means that, instead of an `OUTER JOIN`, an `INNER JOIN` is done, returning only the parent models with at least one matching children. - -Note also that the `where` option used was converted into a condition for the `ON` clause of the `INNER JOIN`. In order to obtain a *top-level* `WHERE` clause, instead of an `ON` clause, something different must be done. This will be shown next. - -#### Referring to other columns - -If you want to apply a `WHERE` clause in an included model referring to a value from an associated model, you can simply use the `Sequelize.col` function, as show in the example below: - -```js -// Find all projects with a least one task where task.state === project.state -Project.findAll({ - include: { - model: Task, - where: { - state: Sequelize.col('project.state') - } - } -}) -``` - -### Complex where clauses at the top-level - -To obtain top-level `WHERE` clauses that involve nested columns, Sequelize provides a way to reference nested columns: the `'$nested.column$'` syntax. - -It can be used, for example, to move the where conditions from an included model from the `ON` condition to a top-level `WHERE` clause. - -```js -User.findAll({ - where: { - '$Instruments.size$': { [Op.ne]: 'small' } - }, - include: [{ - model: Tool, - as: 'Instruments' - }] -}); -``` - -Generated SQL: - -```sql -SELECT - `user`.`id`, - `user`.`name`, - `Instruments`.`id` AS `Instruments.id`, - `Instruments`.`name` AS `Instruments.name`, - `Instruments`.`size` AS `Instruments.size`, - `Instruments`.`userId` AS `Instruments.userId` -FROM `users` AS `user` -LEFT OUTER JOIN `tools` AS `Instruments` ON - `user`.`id` = `Instruments`.`userId` -WHERE `Instruments`.`size` != 'small'; -``` - -The `$nested.column$` syntax also works for columns that are nested several levels deep, such as `$some.super.deeply.nested.column$`. Therefore, you can use this to make complex filters on deeply nested columns. - -For a better understanding of all differences between the inner `where` option (used inside an `include`), with and without the `required` option, and a top-level `where` using the `$nested.column$` syntax, below we have four examples for you: - -```js -// Inner where, with default `required: true` -await User.findAll({ - include: { - model: Tool, - as: 'Instruments', - where: { - size: { [Op.ne]: 'small' } - } - } -}); - -// Inner where, `required: false` -await User.findAll({ - include: { - model: Tool, - as: 'Instruments', - where: { - size: { [Op.ne]: 'small' } - }, - required: false - } -}); - -// Top-level where, with default `required: false` -await User.findAll({ - where: { - '$Instruments.size$': { [Op.ne]: 'small' } - }, - include: { - model: Tool, - as: 'Instruments' - } -}); - -// Top-level where, `required: true` -await User.findAll({ - where: { - '$Instruments.size$': { [Op.ne]: 'small' } - }, - include: { - model: Tool, - as: 'Instruments', - required: true - } -}); -``` - -Generated SQLs, in order: - -```sql --- Inner where, with default `required: true` -SELECT [...] FROM `users` AS `user` -INNER JOIN `tools` AS `Instruments` ON - `user`.`id` = `Instruments`.`userId` - AND `Instruments`.`size` != 'small'; - --- Inner where, `required: false` -SELECT [...] FROM `users` AS `user` -LEFT OUTER JOIN `tools` AS `Instruments` ON - `user`.`id` = `Instruments`.`userId` - AND `Instruments`.`size` != 'small'; - --- Top-level where, with default `required: false` -SELECT [...] FROM `users` AS `user` -LEFT OUTER JOIN `tools` AS `Instruments` ON - `user`.`id` = `Instruments`.`userId` -WHERE `Instruments`.`size` != 'small'; - --- Top-level where, `required: true` -SELECT [...] FROM `users` AS `user` -INNER JOIN `tools` AS `Instruments` ON - `user`.`id` = `Instruments`.`userId` -WHERE `Instruments`.`size` != 'small'; -``` - -### Fetching with `RIGHT OUTER JOIN` (MySQL, MariaDB, PostgreSQL and MSSQL only) - -By default, associations are loaded using a `LEFT OUTER JOIN` - that is to say it only includes records from the parent table. You can change this behavior to a `RIGHT OUTER JOIN` by passing the `right` option, if the dialect you are using supports it. - -Currently, SQLite does not support [right joins](https://www.sqlite.org/omitted.html). - -*Note:* `right` is only respected if `required` is false. - -```js -User.findAll({ - include: [{ - model: Task // will create a left join - }] -}); -User.findAll({ - include: [{ - model: Task, - right: true // will create a right join - }] -}); -User.findAll({ - include: [{ - model: Task, - required: true, - right: true // has no effect, will create an inner join - }] -}); -User.findAll({ - include: [{ - model: Task, - where: { name: { [Op.ne]: 'empty trash' } }, - right: true // has no effect, will create an inner join - }] -}); -User.findAll({ - include: [{ - model: Tool, - where: { name: { [Op.ne]: 'empty trash' } }, - required: false // will create a left join - }] -}); -User.findAll({ - include: [{ - model: Tool, - where: { name: { [Op.ne]: 'empty trash' } }, - required: false - right: true // will create a right join - }] -}); -``` - -## Multiple eager loading - -The `include` option can receive an array in order to fetch multiple associated models at once: - -```js -Foo.findAll({ - include: [ - { - model: Bar, - required: true - }, - { - model: Baz, - where: /* ... */ - }, - Qux // Shorthand syntax for { model: Qux } also works here - ] -}) -``` - -## Eager loading with Many-to-Many relationships - -When you perform eager loading on a model with a Belongs-to-Many relationship, Sequelize will fetch the junction table data as well, by default. For example: - -```js -const Foo = sequelize.define('Foo', { name: DataTypes.TEXT }); -const Bar = sequelize.define('Bar', { name: DataTypes.TEXT }); -Foo.belongsToMany(Bar, { through: 'Foo_Bar' }); -Bar.belongsToMany(Foo, { through: 'Foo_Bar' }); - -await sequelize.sync(); -const foo = await Foo.create({ name: 'foo' }); -const bar = await Bar.create({ name: 'bar' }); -await foo.addBar(bar); -const fetchedFoo = await Foo.findOne({ include: Bar }); -console.log(JSON.stringify(fetchedFoo, null, 2)); -``` - -Output: - -```json -{ - "id": 1, - "name": "foo", - "Bars": [ - { - "id": 1, - "name": "bar", - "Foo_Bar": { - "FooId": 1, - "BarId": 1 - } - } - ] -} -``` - -Note that every bar instance eager loaded into the `"Bars"` property has an extra property called `Foo_Bar` which is the relevant Sequelize instance of the junction model. By default, Sequelize fetches all attributes from the junction table in order to build this extra property. - -However, you can specify which attributes you want fetched. This is done with the `attributes` option applied inside the `through` option of the include. For example: - -```js -Foo.findAll({ - include: [{ - model: Bar, - through: { - attributes: [/* list the wanted attributes here */] - } - }] -}); -``` - -If you don't want anything from the junction table, you can explicitly provide an empty array to the `attributes` option inside the `through` option of the `include` option, and in this case nothing will be fetched and the extra property will not even be created: - -```js -Foo.findOne({ - include: { - model: Bar, - through: { - attributes: [] - } - } -}); -``` - -Output: - -```json -{ - "id": 1, - "name": "foo", - "Bars": [ - { - "id": 1, - "name": "bar" - } - ] -} -``` - -Whenever including a model from a Many-to-Many relationship, you can also apply a filter on the junction table. This is done with the `where` option applied inside the `through` option of the include. For example: - -```js -User.findAll({ - include: [{ - model: Project, - through: { - where: { - // Here, `completed` is a column present at the junction table - completed: true - } - } - }] -}); -``` - -Generated SQL (using SQLite): - -```sql -SELECT - `User`.`id`, - `User`.`name`, - `Projects`.`id` AS `Projects.id`, - `Projects`.`name` AS `Projects.name`, - `Projects->User_Project`.`completed` AS `Projects.User_Project.completed`, - `Projects->User_Project`.`UserId` AS `Projects.User_Project.UserId`, - `Projects->User_Project`.`ProjectId` AS `Projects.User_Project.ProjectId` -FROM `Users` AS `User` -LEFT OUTER JOIN `User_Projects` AS `Projects->User_Project` ON - `User`.`id` = `Projects->User_Project`.`UserId` -LEFT OUTER JOIN `Projects` AS `Projects` ON - `Projects`.`id` = `Projects->User_Project`.`ProjectId` AND - `Projects->User_Project`.`completed` = 1; -``` - -## Including everything - -To include all associated models, you can use the `all` and `nested` options: - -```js -// Fetch all models associated with User -User.findAll({ include: { all: true }}); - -// Fetch all models associated with User and their nested associations (recursively) -User.findAll({ include: { all: true, nested: true }}); -``` - -## Including soft deleted records - -In case you want to eager load soft deleted records you can do that by setting `include.paranoid` to `false`: - -```js -User.findAll({ - include: [{ - model: Tool, - as: 'Instruments', - where: { size: { [Op.ne]: 'small' } }, - paranoid: false - }] -}); -``` - -## Ordering eager loaded associations - -When you want to apply `ORDER` clauses to eager loaded models, you must use the top-level `order` option with augmented arrays, starting with the specification of the nested model you want to sort. - -This is better understood with examples. - -```js -Company.findAll({ - include: Division, - order: [ - // We start the order array with the model we want to sort - [Division, 'name', 'ASC'] - ] -}); -Company.findAll({ - include: Division, - order: [ - [Division, 'name', 'DESC'] - ] -}); -Company.findAll({ - // If the include uses an alias... - include: { model: Division, as: 'Div' }, - order: [ - // ...we use the same syntax from the include - // in the beginning of the order array - [{ model: Division, as: 'Div' }, 'name', 'DESC'] - ] -}); - -Company.findAll({ - // If we have includes nested in several levels... - include: { - model: Division, - include: Department - }, - order: [ - // ... we replicate the include chain of interest - // at the beginning of the order array - [Division, Department, 'name', 'DESC'] - ] -}); -``` - -In the case of many-to-many relationships, you are also able to sort by attributes in the through table. For example, assuming we have a Many-to-Many relationship between `Division` and `Department` whose junction model is `DepartmentDivision`, you can do: - -```js -Company.findAll({ - include: { - model: Division, - include: Department - }, - order: [ - [Division, DepartmentDivision, 'name', 'ASC'] - ] -}); -``` - -In all the above examples, you have noticed that the `order` option is used at the top-level. The only situation in which `order` also works inside the include option is when `separate: true` is used. In that case, the usage is as follows: - -```js -// This only works for `separate: true` (which in turn -// only works for has-many relationships). -User.findAll({ - include: { - model: Post, - separate: true, - order: [ - ['createdAt', 'DESC'] - ] - } -}); -``` - -### Complex ordering involving sub-queries - -Take a look at the [guide on sub-queries](../other-topics/sub-queries.md) for an example of how to use a sub-query to assist a more complex ordering. - -## Nested eager loading - -You can use nested eager loading to load all related models of a related model: - -```js -const users = await User.findAll({ - include: { - model: Tool, - as: 'Instruments', - include: { - model: Teacher, - include: [ /* etc */ ] - } - } -}); -console.log(JSON.stringify(users, null, 2)); -``` - -Output: - -```json -[{ - "name": "John Doe", - "id": 1, - "Instruments": [{ // 1:M and N:M association - "name": "Scissor", - "id": 1, - "userId": 1, - "Teacher": { // 1:1 association - "name": "Jimi Hendrix" - } - }] -}] -``` - -This will produce an outer join. However, a `where` clause on a related model will create an inner join and return only the instances that have matching sub-models. To return all parent instances, you should add `required: false`. - -```js -User.findAll({ - include: [{ - model: Tool, - as: 'Instruments', - include: [{ - model: Teacher, - where: { - school: "Woodstock Music School" - }, - required: false - }] - }] -}); -``` - -The query above will return all users, and all their instruments, but only those teachers associated with `Woodstock Music School`. - -## Using `findAndCountAll` with includes - -The `findAndCountAll` utility function supports includes. Only the includes that are marked as `required` will be considered in `count`. For example, if you want to find and count all users who have a profile: - -```js -User.findAndCountAll({ - include: [ - { model: Profile, required: true } - ], - limit: 3 -}); -``` - -Because the include for `Profile` has `required` set it will result in an inner join, and only the users who have a profile will be counted. If we remove `required` from the include, both users with and without profiles will be counted. Adding a `where` clause to the include automatically makes it required: - -```js -User.findAndCountAll({ - include: [ - { model: Profile, where: { active: true } } - ], - limit: 3 -}); -``` - -The query above will only count users who have an active profile, because `required` is implicitly set to true when you add a where clause to the include. diff --git a/docs/advanced-association-concepts/polymorphic-associations.md b/docs/advanced-association-concepts/polymorphic-associations.md deleted file mode 100644 index 4ae9855c..00000000 --- a/docs/advanced-association-concepts/polymorphic-associations.md +++ /dev/null @@ -1,430 +0,0 @@ ---- -sidebar_position: 5 -title: Polymorphic Associations ---- - -_**Note:** the usage of polymorphic associations in Sequelize, as outlined in this guide, should be done with caution. Don't just copy-paste code from here, otherwise you might easily make mistakes and introduce bugs in your code. Make sure you understand what is going on._ - -## Concept - -A **polymorphic association** consists on two (or more) associations happening with the same foreign key. - -For example, consider the models `Image`, `Video` and `Comment`. The first two represent something that a user might post. We want to allow comments to be placed in both of them. This way, we immediately think of establishing the following associations: - -* A One-to-Many association between `Image` and `Comment`: - - ```js - Image.hasMany(Comment); - Comment.belongsTo(Image); - ``` - -* A One-to-Many association between `Video` and `Comment`: - - ```js - Video.hasMany(Comment); - Comment.belongsTo(Video); - ``` - -However, the above would cause Sequelize to create two foreign keys on the `Comment` table: `ImageId` and `VideoId`. This is not ideal because this structure makes it look like a comment can be attached at the same time to one image and one video, which isn't true. Instead, what we really want here is precisely a polymorphic association, in which a `Comment` points to a single **Commentable**, an abstract polymorphic entity that represents one of `Image` or `Video`. - -Before proceeding to how to configure such an association, let's see how using it looks like: - -```js -const image = await Image.create({ url: "https://placekitten.com/408/287" }); -const comment = await image.createComment({ content: "Awesome!" }); - -console.log(comment.commentableId === image.id); // true - -// We can also retrieve which type of commentable a comment is associated to. -// The following prints the model name of the associated commentable instance. -console.log(comment.commentableType); // "Image" - -// We can use a polymorphic method to retrieve the associated commentable, without -// having to worry whether it's an Image or a Video. -const associatedCommentable = await comment.getCommentable(); - -// In this example, `associatedCommentable` is the same thing as `image`: -const isDeepEqual = require('deep-equal'); -console.log(isDeepEqual(image, commentable)); // true -``` - -## Configuring a One-to-Many polymorphic association - -To setup the polymorphic association for the example above (which is an example of One-to-Many polymorphic association), we have the following steps: - -* Define a string field called `commentableType` in the `Comment` model; -* Define the `hasMany` and `belongsTo` association between `Image`/`Video` and `Comment`: - * Disabling constraints (i.e. using `{ constraints: false }`), since the same foreign key is referencing multiple tables; - * Specifying the appropriate [association scopes](./association-scopes.md); -* To properly support lazy loading, define a new instance method on the `Comment` model called `getCommentable` which calls, under the hood, the correct mixin to fetch the appropriate commentable; -* To properly support eager loading, define an `afterFind` hook on the `Comment` model that automatically populates the `commentable` field in every instance; -* To prevent bugs/mistakes in eager loading, you can also delete the concrete fields `image` and `video` from Comment instances in the same `afterFind` hook, leaving only the abstract `commentable` field available. - -Here is an example: - -```js -// Helper function -const uppercaseFirst = str => `${str[0].toUpperCase()}${str.substr(1)}`; - -class Image extends Model {} -Image.init({ - title: DataTypes.STRING, - url: DataTypes.STRING -}, { sequelize, modelName: 'image' }); - -class Video extends Model {} -Video.init({ - title: DataTypes.STRING, - text: DataTypes.STRING -}, { sequelize, modelName: 'video' }); - -class Comment extends Model { - getCommentable(options) { - if (!this.commentableType) return Promise.resolve(null); - const mixinMethodName = `get${uppercaseFirst(this.commentableType)}`; - return this[mixinMethodName](options); - } -} -Comment.init({ - title: DataTypes.STRING, - commentableId: DataTypes.INTEGER, - commentableType: DataTypes.STRING -}, { sequelize, modelName: 'comment' }); - -Image.hasMany(Comment, { - foreignKey: 'commentableId', - constraints: false, - scope: { - commentableType: 'image' - } -}); -Comment.belongsTo(Image, { foreignKey: 'commentableId', constraints: false }); - -Video.hasMany(Comment, { - foreignKey: 'commentableId', - constraints: false, - scope: { - commentableType: 'video' - } -}); -Comment.belongsTo(Video, { foreignKey: 'commentableId', constraints: false }); - -Comment.hooks.addListener("afterFind", findResult => { - if (!Array.isArray(findResult)) findResult = [findResult]; - for (const instance of findResult) { - if (instance.commentableType === "image" && instance.image !== undefined) { - instance.commentable = instance.image; - } else if (instance.commentableType === "video" && instance.video !== undefined) { - instance.commentable = instance.video; - } - // To prevent mistakes: - delete instance.image; - delete instance.dataValues.image; - delete instance.video; - delete instance.dataValues.video; - } -}); -``` - -Since the `commentableId` column references several tables (two in this case), we cannot add a `REFERENCES` constraint to it. This is why the `constraints: false` option was used. - -Note that, in the code above: - -* The *Image -> Comment* association defined an association scope: `{ commentableType: 'image' }` -* The *Video -> Comment* association defined an association scope: `{ commentableType: 'video' }` - -These scopes are automatically applied when using the association functions (as explained in the [Association Scopes](./association-scopes.md) guide). Some examples are below, with their generated SQL statements: - -* `image.getComments()`: - - ```sql - SELECT "id", "title", "commentableType", "commentableId", "createdAt", "updatedAt" - FROM "comments" AS "comment" - WHERE "comment"."commentableType" = 'image' AND "comment"."commentableId" = 1; - ``` - - Here we can see that `` `comment`.`commentableType` = 'image'`` was automatically added to the `WHERE` clause of the generated SQL. This is exactly the behavior we want. - -* `image.createComment({ title: 'Awesome!' })`: - - ```sql - INSERT INTO "comments" ( - "id", "title", "commentableType", "commentableId", "createdAt", "updatedAt" - ) VALUES ( - DEFAULT, 'Awesome!', 'image', 1, - '2018-04-17 05:36:40.454 +00:00', '2018-04-17 05:36:40.454 +00:00' - ) RETURNING *; - ``` - -* `image.addComment(comment)`: - - ```sql - UPDATE "comments" - SET "commentableId"=1, "commentableType"='image', "updatedAt"='2018-04-17 05:38:43.948 +00:00' - WHERE "id" IN (1) - ``` - -### Polymorphic lazy loading - -The `getCommentable` instance method on `Comment` provides an abstraction for lazy loading the associated commentable - working whether the comment belongs to an Image or a Video. - -It works by simply converting the `commentableType` string into a call to the correct mixin (either `getImage` or `getVideo`). - -Note that the `getCommentable` implementation above: - -* Returns `null` when no association is present (which is good); -* Allows you to pass an options object to `getCommentable(options)`, just like any other standard Sequelize method. This is useful to specify where-conditions or includes, for example. - -### Polymorphic eager loading - -Now, we want to perform a polymorphic eager loading of the associated commentables for one (or more) comments. We want to achieve something similar to the following idea: - -```js -const comment = await Comment.findOne({ - include: [ /* What to put here? */ ] -}); -console.log(comment.commentable); // This is our goal -``` - -The solution is to tell Sequelize to include both Images and Videos, so that our `afterFind` hook defined above will do the work, automatically adding the `commentable` field to the instance object, providing the abstraction we want. - -For example: - -```js -const comments = await Comment.findAll({ - include: [Image, Video] -}); -for (const comment of comments) { - const message = `Found comment #${comment.id} with ${comment.commentableType} commentable:`; - console.log(message, comment.commentable.toJSON()); -} -``` - -Output example: - -```text -Found comment #1 with image commentable: { id: 1, - title: 'Meow', - url: 'https://placekitten.com/408/287', - createdAt: 2019-12-26T15:04:53.047Z, - updatedAt: 2019-12-26T15:04:53.047Z } -``` - -### Caution - possibly invalid eager/lazy loading! - -Consider a comment `Foo` whose `commentableId` is 2 and `commentableType` is `image`. Consider also that `Image A` and `Video X` both happen to have an id equal to 2. Conceptually, it is clear that `Video X` is not associated to `Foo`, because even though its id is 2, the `commentableType` of `Foo` is `image`, not `video`. However, this distinction is made by Sequelize only at the level of the abstractions performed by `getCommentable` and the hook we created above. - -This means that if you call `Comment.findAll({ include: Video })` in the situation above, `Video X` will be eager loaded into `Foo`. Thankfully, our `afterFind` hook will delete it automatically, to help prevent bugs, but regardless it is important that you understand what is going on. - -The best way to prevent this kind of mistake is to **avoid using the concrete accessors and mixins directly at all costs** (such as `.image`, `.getVideo()`, `.setImage()`, etc), always preferring the abstractions we created, such as `.getCommentable()` and `.commentable`. If you really need to access eager-loaded `.image` and `.video` for some reason, make sure you wrap that in a type check such as `comment.commentableType === 'image'`. - -## Configuring a Many-to-Many polymorphic association - -In the above example, we had the models `Image` and `Video` being abstractly called *commentables*, with one *commentable* having many comments. However, one given comment would belong to a single *commentable* - this is why the whole situation is a One-to-Many polymorphic association. - -Now, to consider a Many-to-Many polymorphic association, instead of considering comments, we will consider tags. For convenience, instead of calling Image and Video as *commentables*, we will now call them *taggables*. One *taggable* may have several tags, and at the same time one tag can be placed in several *taggables*. - -The setup for this goes as follows: - -* Define the junction model explicitly, specifying the two foreign keys as `tagId` and `taggableId` (this way it is a junction model for a Many-to-Many relationship between `Tag` and the abstract concept of *taggable*); -* Define a string field called `taggableType` in the junction model; -* Define the `belongsToMany` associations between the two models and `Tag`: - * Disabling constraints (i.e. using `{ constraints: false }`), since the same foreign key is referencing multiple tables; - * Specifying the appropriate [association scopes](./association-scopes.md); -* Define a new instance method on the `Tag` model called `getTaggables` which calls, under the hood, the correct mixin to fetch the appropriate taggables. - -Implementation: - -```js -class Tag extends Model { - getTaggables(options) { - const images = await this.getImages(options); - const videos = await this.getVideos(options); - // Concat images and videos in a single array of taggables - return images.concat(videos); - } -} -Tag.init({ - name: DataTypes.STRING -}, { sequelize, modelName: 'tag' }); - -// Here we define the junction model explicitly -class Tag_Taggable extends Model {} -Tag_Taggable.init({ - tagId: { - type: DataTypes.INTEGER, - unique: 'tt_unique_constraint' - }, - taggableId: { - type: DataTypes.INTEGER, - unique: 'tt_unique_constraint', - references: null - }, - taggableType: { - type: DataTypes.STRING, - unique: 'tt_unique_constraint' - } -}, { sequelize, modelName: 'tag_taggable' }); - -Image.belongsToMany(Tag, { - through: { - model: Tag_Taggable, - unique: false, - scope: { - taggableType: 'image' - } - }, - foreignKey: 'taggableId', - constraints: false -}); -Tag.belongsToMany(Image, { - through: { - model: Tag_Taggable, - unique: false - }, - foreignKey: 'tagId', - constraints: false -}); - -Video.belongsToMany(Tag, { - through: { - model: Tag_Taggable, - unique: false, - scope: { - taggableType: 'video' - } - }, - foreignKey: 'taggableId', - constraints: false -}); -Tag.belongsToMany(Video, { - through: { - model: Tag_Taggable, - unique: false - }, - foreignKey: 'tagId', - constraints: false -}); -``` - -The `constraints: false` option disables references constraints, as the `taggableId` column references several tables, we cannot add a `REFERENCES` constraint to it. - -Note that: - -* The *Image -> Tag* association defined an association scope: `{ taggableType: 'image' }` -* The *Video -> Tag* association defined an association scope: `{ taggableType: 'video' }` - -These scopes are automatically applied when using the association functions. Some examples are below, with their generated SQL statements: - -* `image.getTags()`: - - ```sql - SELECT - `tag`.`id`, - `tag`.`name`, - `tag`.`createdAt`, - `tag`.`updatedAt`, - `tag_taggable`.`tagId` AS `tag_taggable.tagId`, - `tag_taggable`.`taggableId` AS `tag_taggable.taggableId`, - `tag_taggable`.`taggableType` AS `tag_taggable.taggableType`, - `tag_taggable`.`createdAt` AS `tag_taggable.createdAt`, - `tag_taggable`.`updatedAt` AS `tag_taggable.updatedAt` - FROM `tags` AS `tag` - INNER JOIN `tag_taggables` AS `tag_taggable` ON - `tag`.`id` = `tag_taggable`.`tagId` AND - `tag_taggable`.`taggableId` = 1 AND - `tag_taggable`.`taggableType` = 'image'; - ``` - - Here we can see that `` `tag_taggable`.`taggableType` = 'image'`` was automatically added to the `WHERE` clause of the generated SQL. This is exactly the behavior we want. - -* `tag.getTaggables()`: - - ```sql - SELECT - `image`.`id`, - `image`.`url`, - `image`.`createdAt`, - `image`.`updatedAt`, - `tag_taggable`.`tagId` AS `tag_taggable.tagId`, - `tag_taggable`.`taggableId` AS `tag_taggable.taggableId`, - `tag_taggable`.`taggableType` AS `tag_taggable.taggableType`, - `tag_taggable`.`createdAt` AS `tag_taggable.createdAt`, - `tag_taggable`.`updatedAt` AS `tag_taggable.updatedAt` - FROM `images` AS `image` - INNER JOIN `tag_taggables` AS `tag_taggable` ON - `image`.`id` = `tag_taggable`.`taggableId` AND - `tag_taggable`.`tagId` = 1; - - SELECT - `video`.`id`, - `video`.`url`, - `video`.`createdAt`, - `video`.`updatedAt`, - `tag_taggable`.`tagId` AS `tag_taggable.tagId`, - `tag_taggable`.`taggableId` AS `tag_taggable.taggableId`, - `tag_taggable`.`taggableType` AS `tag_taggable.taggableType`, - `tag_taggable`.`createdAt` AS `tag_taggable.createdAt`, - `tag_taggable`.`updatedAt` AS `tag_taggable.updatedAt` - FROM `videos` AS `video` - INNER JOIN `tag_taggables` AS `tag_taggable` ON - `video`.`id` = `tag_taggable`.`taggableId` AND - `tag_taggable`.`tagId` = 1; - ``` - -Note that the above implementation of `getTaggables()` allows you to pass an options object to `getCommentable(options)`, just like any other standard Sequelize method. This is useful to specify where-conditions or includes, for example. - -### Applying scopes on the target model - -In the example above, the `scope` options (such as `scope: { taggableType: 'image' }`) were applied to the *through* model, not the *target* model, since it was used under the `through` option. - -We can also apply an association scope on the target model. We can even do both at the same time. - -To illustrate this, consider an extension of the above example between tags and taggables, where each tag has a status. This way, to get all pending tags of an image, we could establish another `belognsToMany` relationship between `Image` and `Tag`, this time applying a scope on the through model and another scope on the target model: - -```js -Image.belongsToMany(Tag, { - through: { - model: Tag_Taggable, - unique: false, - scope: { - taggableType: 'image' - } - }, - scope: { - status: 'pending' - }, - as: 'pendingTags', - foreignKey: 'taggableId', - constraints: false -}); -``` - -This way, when calling `image.getPendingTags()`, the following SQL query will be generated: - -```sql -SELECT - `tag`.`id`, - `tag`.`name`, - `tag`.`status`, - `tag`.`createdAt`, - `tag`.`updatedAt`, - `tag_taggable`.`tagId` AS `tag_taggable.tagId`, - `tag_taggable`.`taggableId` AS `tag_taggable.taggableId`, - `tag_taggable`.`taggableType` AS `tag_taggable.taggableType`, - `tag_taggable`.`createdAt` AS `tag_taggable.createdAt`, - `tag_taggable`.`updatedAt` AS `tag_taggable.updatedAt` -FROM `tags` AS `tag` -INNER JOIN `tag_taggables` AS `tag_taggable` ON - `tag`.`id` = `tag_taggable`.`tagId` AND - `tag_taggable`.`taggableId` = 1 AND - `tag_taggable`.`taggableType` = 'image' -WHERE ( - `tag`.`status` = 'pending' -); -``` - -We can see that both scopes were applied automatically: - -* `` `tag_taggable`.`taggableType` = 'image'`` was added automatically to the `INNER JOIN`; -* `` `tag`.`status` = 'pending'`` was added automatically to an outer where clause. diff --git a/docs/core-concepts/_category_.json b/docs/associations/_category_.json similarity index 67% rename from docs/core-concepts/_category_.json rename to docs/associations/_category_.json index aec74de9..db1f1d5b 100644 --- a/docs/core-concepts/_category_.json +++ b/docs/associations/_category_.json @@ -1,6 +1,6 @@ { - "position": 3, - "label": "Core Concepts", + "position": 6, + "label": "Associations", "collapsible": true, "collapsed": false, "link": { diff --git a/docs/associations/association-scopes.md b/docs/associations/association-scopes.md new file mode 100644 index 00000000..9e28790e --- /dev/null +++ b/docs/associations/association-scopes.md @@ -0,0 +1,128 @@ +--- +title: Association Scopes +--- + +:::info + +This section concerns association scopes, not to be confused with [model scopes](../other-topics/scopes.md). + +::: + +Association scopes are a way to automatically apply default filters on associated models. + +For instance, you could define an association from `City` to `Restaurant` with a scope that only returns restaurants that are open: + +```js +class City extends Model { + @Attribute(DataTypes.STRING) + name; + + /** this association returns all restaurants */ + @HasMany(() => Restaurant, 'cityId') + restaurants; + + /** this association only returns open restaurants */ + @HasMany(() => Restaurant, { + foreignKey: 'cityId', + // highlight-next-line + scope: { status: 'open' }, + }) + openRestaurants; +} + +class Restaurant extends Model { + @Attribute(DataTypes.STRING) + status; +} + +const city = await City.findByPk(1); + +// this will return all restaurants +const restaurants = await city.getRestaurants(); + +// this will return only open restaurants +const openRestaurants = await city.getOpenRestaurants(); +``` + +This last query would roughly generate the following SQL: + +```sql +SELECT * FROM `restaurants` WHERE `restaurants`.`status` = 'open' AND `restaurants`.`cityId` = 1; +``` + +## BelongsToMany scope + +All associations support specifying a scope to filter the target model, but the `BelongsToMany` association +also supports specifying a scope to filter the join table. This is useful when you want to filter based on extra information +stored in the join table. + +It is done by setting the `through.scope` option. + +Here is a simple example. We want to store which person worked on a game, but we also want to store the role they had in its creation: + +```js +class GameAuthor extends Model { + @Attribute(DataTypes.STRING) + role; +} + +class Person extends Model {} + +class Game extends Model { + /** This association will list everyone that worked on the game */ + @BelongsToMany(() => Person, { + through: GameAuthor + }) + allAuthors; +} +``` + +In the above example, we can use the `allAuthors` association to list everyone that worked on the game, but we can +also add other associations to filter the authors based on their role: + +```js +class Game extends Model { + /** This association will list everyone that worked on the game */ + @BelongsToMany(() => Person, { + through: GameAuthor, + foreignKey: 'gameId', + otherKey: 'personId', + }) + allAuthors; + + /** This association will list everyone that worked on the game as a programmer */ + @BelongsToMany(() => Person, { + through: { + model: GameAuthor, + foreignKey: 'gameId', + otherKey: 'personId', + // highlight-next-line + scope: { role: 'programmer' }, + }, + }) + programmers; + + /** This association will list everyone that worked on the game as a designer */ + @BelongsToMany(() => Person, { + through: { + model: GameAuthor, + foreignKey: 'gameId', + otherKey: 'personId', + // highlight-next-line + scope: { role: 'designer' }, + }, + }) + designers; +} + +const game = await Game.findByPk(1); + +// this will return all authors +const allAuthors = await game.getAllAuthors(); + +// this will return only programmers +const programmers = await game.getProgrammers(); + +// this will return only designers +const designers = await game.getDesigners(); +``` diff --git a/docs/associations/basics.md b/docs/associations/basics.md new file mode 100644 index 00000000..aee3872e --- /dev/null +++ b/docs/associations/basics.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 1 +title: Basics +--- + +# Association Basics + +Sequelize provides what are called __associations__. +These can be declared on your models to define common [__relationships__](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)) between your tables. + +The two concepts are closely related, but not the same. __Associations__ are defined in JavaScript between your _models_, while +__relationships__ are defined in your database between your _tables_. + +Sequelize supports the standard associations: [One-To-One](https://en.wikipedia.org/wiki/One-to-one_%28data_model%29), [One-To-Many](https://en.wikipedia.org/wiki/One-to-many_%28data_model%29) and [Many-To-Many](https://en.wikipedia.org/wiki/Many-to-many_%28data_model%29). + +## One-to-one Relationships + +In a One-To-One relationship, a row of one table is associated with a single row of another table. + +The most common type of One-To-One relationship is one where one side is mandatory, and the other side is optional. +For instance, a driving license always belongs to a single person, but a person can have zero or one driving licenses (from the same place). + +```mermaid +erDiagram + people ||--o| driving_licenses : drivingLicense +``` + +One-To-One relationships can be created by using __the [`HasOne`](./has-one.md) association__. + +## One-to-many Relationships + +In a One-To-Many relationship, a row of one table is associated with _zero, one or more_ rows of another table. + +For instance, a person is always born in one city, but a city can have zero or more people born in it. + +```mermaid +erDiagram + people ||--o{ cities : birthplace +``` + +One-To-Many relationships can be created by using __the [`HasMany`](./has-many.md) association__. + +## Many-to-many Relationships + +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. + +```mermaid +erDiagram + people }o--o{ toots : likedToots +``` + +Many-To-Many relationships can be created by using __the [`BelongsToMany`](./belongs-to-many.md) association__. diff --git a/docs/associations/belongs-to-many.md b/docs/associations/belongs-to-many.md new file mode 100644 index 00000000..68f5084a --- /dev/null +++ b/docs/associations/belongs-to-many.md @@ -0,0 +1,542 @@ +--- +sidebar_position: 5 +title: BelongsToMany +--- + +# The BelongsToMany Association + +The BelongsToMany association is used to create a [Many-To-Many relationship](https://en.wikipedia.org/wiki/Many-to-many_(data_model)) 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. + +```mermaid +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. + +```mermaid +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. + +## Defining the Association + +Here is how you would define the `Person` and `Toot` models in Sequelize: + +```ts +import { Model, InferAttributes, InferCreationAttributes, NonAttribute } from '@sequelize/core'; +import { BelongsToMany } from '@sequelize/core/decorators-legacy'; + +class Person extends Model, InferCreationAttributes> { + // highlight-start + @BelongsToMany(() => Toot, { + through: 'LikedToot', + }) + declare likedToots?: NonAttribute; + // highlight-end +} + +class Toot extends Model, InferCreationAttributes> {} +``` + +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): + +```ts +class Person extends Model, InferCreationAttributes> { + @BelongsToMany(() => Toot, { + // You should name this LikedToot instead. + // error-next-line + through: 'liked_toots', + }) + declare likedToots?: NonAttribute; +} +``` + +::: + +## 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. + +```ts +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, InferCreationAttributes> { + @BelongsToMany(() => Toot, { + through: () => LikedToot, + }) + declare likedToots?: NonAttribute; +} + +class LikedToot extends Model, InferCreationAttributes> { + declare likerId: number; + declare likedTootId: number; +} + +class Toot extends Model, InferCreationAttributes> {} +``` + +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](../models/defining-models.mdx) 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: + +```ts +import { Model, InferAttributes, InferCreationAttributes, NonAttribute } from '@sequelize/core'; +import { BelongsToMany } from '@sequelize/core/decorators-legacy'; + +class Person extends Model, InferCreationAttributes> { + @BelongsToMany(() => Toot, { + through: 'LikedToot', + inverse: { + as: 'likers', + }, + }) + declare likedToots?: NonAttribute; +} + +class Toot extends Model, InferCreationAttributes> { + /** Declared by {@link Person.likedToots} */ + declare likers?: NonAttribute; +} +``` + +The above would result in the following model configuration: + +```mermaid +erDiagram + Person }o--o{ Toot : "⬇️ likedToots / ⬆️ likers" +``` + +## 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](./has-many.md) association going from the Source Model to the Through Model. +- 2️⃣ One [BelongsTo](./belongs-to.md) association going from the Through Model to the Source Model. +- 3️⃣ One [HasMany](./has-many.md) association going from the Target Model to the Through Model. +- 4️⃣ One [BelongsTo](./belongs-to.md) association going from the Through Model to the Target Model. + +```mermaid +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: + +```ts +class Person extends Model, InferCreationAttributes> { + @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; +} +``` + +## 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. + +```ts +class Person extends Model, InferCreationAttributes> { + @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; +} +``` + +## 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. + +```ts +class Person extends Model, InferCreationAttributes> { + @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; +} +``` + +## 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. + +```ts +class Person extends Model, InferCreationAttributes> { + @BelongsToMany(() => Toot, { + through: { + model: 'LikedToot', + // highlight-next-line + unique: false, + }, + }) + declare likedToots?: NonAttribute; +} +``` + +## Association Methods + +All associations add methods to the source model[^1]. 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`: + +```ts +import { BelongsToManyGetAssociationsMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // highlight-start + declare getBooks: BelongsToManyGetAssociationsMixin; + // 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`. + +If the model is already associated to one or more models, the old associations are removed before the new ones are added. + +```ts +import { BelongsToManySetAssociationsMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // 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`: Associates a single new model. +- `add`: Associates multiple new models. + +```ts +import { BelongsToManyAddAssociationMixin, BelongsToManyAddAssociationsMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // 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`: Removes a single associated model. +- `remove`: Removes multiple associated models. + + +```ts +import { BelongsToManyRemoveAssociationMixin, BelongsToManyRemoveAssociationsMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // 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`. + +```ts +import { BelongsToManyCreateAssociationMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // highlight-start + declare createBook: BelongsToManyCreateAssociationMixin; + // 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. + +```ts +BelongsToManyCreateAssociationMixin + ^ Here +``` + +::: + +### Association Checker (`hasX`) + +The association checker is used to check if a model is associated with another model. It has two versions: + +- `has`: Checks if a single model is associated. +- `has`: Checks whether all the specified models are associated. + +```ts +import { BelongsToManyHasAssociationMixin, BelongsToManyHasAssociationsMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // 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`. + +```ts +import { BelongsToManyCountAssociationsMixin } from '@sequelize/core'; + +class Author extends Model, InferCreationAttributes> { + @BelongsToMany(() => Book, { through: 'BookAuthor' }) + declare books?: NonAttribute; + + // highlight-start + declare countBooks: BelongsToManyCountAssociationsMixin; + // highlight-end +} + +// ... + +const author = await Author.findByPk(1); + +// highlight-start +// Returns the number of associated books +const count = await author.countBooks(); +// highlight-end +``` + +[^1]: The source model is the model that defines the association. diff --git a/docs/associations/belongs-to.md b/docs/associations/belongs-to.md new file mode 100644 index 00000000..2f294df7 --- /dev/null +++ b/docs/associations/belongs-to.md @@ -0,0 +1,262 @@ +--- +sidebar_position: 4 +title: BelongsTo +--- + +# The BelongsTo Association + +The `BelongsTo` association is the association all other associations are based on. It's the simplest form of +association, and is meant to be used as a way to add a foreign key to a model. + +We recommend reading the guides on [`HasOne`](./has-one.md) and [`HasMany`](./has-many.md) before reading this guide. + +## Defining a BelongsTo Association + +The `BelongsTo` association is used on the opposite side of where you would use a `HasOne` or `HasMany` association. +It is capable of creating both One-To-One and One-To-Many relationships. + +For instance, here is how you would create the association we described in the [`HasMany`](./has-many.md) guide, +using a `BelongsTo` association: + +```ts +import { Model, DataTypes, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from '@sequelize/core'; +import { PrimaryKey, Attribute, AutoIncrement, NotNull, BelongsTo } from '@sequelize/core/decorators-legacy'; + +class Post extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; +} + +class Comment extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + @BelongsTo(() => Post, 'postId') + declare post?: NonAttribute; + + // This is the foreign key + @Attribute(DataTypes.INTEGER) + @NotNull + declare postId: number; + // highlight-end +} +``` + +And here is how you would create the association we described in the [`HasOne`](./has-one.md) guide: + +```ts +import { Model, DataTypes, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from '@sequelize/core'; +import { PrimaryKey, Attribute, AutoIncrement, NotNull, HasOne, BelongsTo } from '@sequelize/core/decorators-legacy'; + +class Person extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; +} + +class DrivingLicense extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + @BelongsTo(() => Person, /* foreign key */ 'ownerId') + declare owner?: NonAttribute; + + // This is the foreign key + @Attribute(DataTypes.INTEGER) + @NotNull + declare ownerId: number; + // highlight-end +} +``` + +## Inverse Association + +Unlike the other 3 associations, `BelongsTo` does _not_ automatically create the inverse association, because it does +not know whether it should be a `HasOne` or a `HasMany` association. + +You can configure the inverse association by using the `inverse` option: + +```ts +class Post extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + /** Declared by {@link Comment#post} */ + declare comments?: Comment[]; + // highlight-end +} + +class Comment extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @BelongsTo(() => Post, { + foreignKey: 'postId', + // highlight-start + inverse: { + as: 'comments', + // Either 'hasOne' or 'hasMany' + type: 'hasMany', + }, + // highlight-end + }) + declare post?: NonAttribute; + + // This is the foreign key + @Attribute(DataTypes.INTEGER) + @NotNull + declare postId: number; +} +``` + +## Association Methods + +The `BelongsTo` association adds the following methods to the model it is defined on: + +### Association Getter (`getX`) + +The getter method is used to retrieve the associated model. It is always named `get`. + +```ts +import { BelongsToGetAssociationMixin } from '@sequelize/core'; +class Comment extends Model { + @BelongsTo(() => Post, 'postId') + declare post?: NonAttribute; + + // highlight-start + declare getPost: BelongsToGetAssociationMixin; + // highlight-end +} + +const comment = await Comment.findByPk(1); +const post = await comment.getPost(); +``` + +### Association Setter (`setX`) + +The setter method is used to associate a model with another model. It is always named `set`. + +It is equivalent to setting the foreign key directly, then calling `save`. + +```ts +import { BelongsToSetAssociationMixin } from '@sequelize/core'; + +class Comment extends Model { + @BelongsTo(() => Post, 'postId') + declare post?: NonAttribute; + + // highlight-start + declare setPost: BelongsToSetAssociationMixin; + // highlight-end +} + +const comment = await Comment.findByPk(1); +const post = await Post.findByPk(1); +await comment.setPost(post); + +// Or, if you already have the foreign key +await comment.setPost(1); +``` + +It is also possible to delay the call to `save` by setting the `save` option to `false`, however __this is not very useful__, +as it is equivalent to setting the foreign key directly, but using a (pointlessly) asynchronous method. + +```ts +await comment.setPost(post, { save: false }); +await comment.save(); +``` + +### Association Creator (`createX`) + +The creator method is used to create a new associated model. It is always named `create`. + +It is equivalent to creating a new model, then setting the foreign key, then calling `save`. + +```ts +import { BelongsToCreateAssociationMixin } from '@sequelize/core'; + +class Comment extends Model { + @BelongsTo(() => Post, 'postId') + declare post?: NonAttribute; + + // highlight-start + declare createPost: BelongsToCreateAssociationMixin; + // highlight-end +} + +const comment = await Comment.create({ content: 'This is a comment' }); + +// highlight-start +const post = await comment.createPost({ + title: 'New Post', + content: 'This is a new post', +}); +// highlight-end +``` + +:::caution Inefficient method + +Using this method is discouraged, as it is less efficient than creating the associated model first, +then creating or updating the current model. + +The following code is more efficient: + +```ts +const post = await Post.create({ + title: 'New Post', + content: 'This is a new post', +}); + +const comment = await Comment.create({ + content: 'This is a comment', + postId: post.id, +}); +``` + +Or, if you have defined the inverse association, this is just as efficient: + +```ts +const post = await Post.create({ + title: 'New Post', + content: 'This is a new post', +}); + +const comment = await post.createComment({ + content: 'This is a comment', +}); +``` + +::: + +## Foreign Key targets (`targetKey`) + +By default, Sequelize will use the primary key of the target model as the attribute the foreign key references. +You can customize this by using the `targetKey` option. + +```ts +class Comment extends Model { + declare id: CreationOptional; + + @BelongsTo(() => Post, { + foreignKey: 'postId', + // highlight-next-line + // The foreign key will reference the 'id' attribute of the Post model + targetKey: 'id', + }) + declare post?: NonAttribute; +} +``` \ No newline at end of file diff --git a/docs/associations/faq.md b/docs/associations/faq.md new file mode 100644 index 00000000..ff975db3 --- /dev/null +++ b/docs/associations/faq.md @@ -0,0 +1,102 @@ +# Association FAQ + +## Multiple associations to the same model + +If you need to create multiple [`HasOne`](./has-one.md) or [`HasMany`](./has-many.md) associations to the same model, make sure to name their inverse associations, +otherwise Sequelize will attempt to use the same inverse association for both associations. + +```ts +class Person extends Model, InferCreationAttributes> { + @HasOne(() => DrivingLicense, { + foreignKey: 'ownerId', + // highlight-start + inverse: { + as: 'owner', + }, + // highlight-end + }) + declare currentDrivingLicense?: NonAttribute; +} +``` + +## Self-references + +Any association can be self-referencing. For example, a `Person` model can have a `parent`/`children` association to another `Person` model. + +```ts +class Person extends Model, InferCreationAttributes> { + @BelongsToMany(() => Person, { + // highlight-start + inverse: { + as: 'parents', + }, + // highlight-end + }) + declare children?: NonAttribute; + declare parents?: NonAttribute; +} +``` + +## Composite Foreign Keys + +Composite foreign keys are not currently supported by Sequelize's associations. See [issue #311](https://github.com/sequelize/sequelize/issues/311) for more information. + +## Customizing Foreign Keys + +Sequelize will generate foreign keys automatically, but you can customize how. The `foreignKey` option (as well as `otherKey` in [`BelongsToMany`](./belongs-to-many.md)) +can be set to a string to specify the name of the foreign key, or to an object to specify the name of the foreign key and other options. + +When set to an object, the `foreignKey` option accepts all options that regular attributes accept, including `allowNull` and `defaultValue`. +See the [API reference](pathname:///api/v7/interfaces/_sequelize_core.index.ForeignKeyOptions.html). + +```ts +class Person extends Model { + @HasOne(() => DrivingLicense, { + foreignKey: { + name: 'ownerId', + columnName: 'owner_id', + }, + }) + declare drivingLicense?: NonAttribute; +} +``` + +You can also use [Attribute Decorators](../models/defining-models.mdx) on your foreign key attribute: + +```ts +class Person extends Model { + @HasOne(() => DrivingLicense, 'ownerId') + declare drivingLicense?: NonAttribute; +} + +class DrivingLicense extends Model { + @Attribute({ + columnName: 'owner_id', + }) + declare ownerId: number; +} +``` + +### `onDelete` and `onUpdate` + +One of the most common use cases for customizing foreign keys is to set the `onDelete` and `onUpdate` behaviors. + +```ts +class Person extends Model { + @HasOne(() => DrivingLicense, { + foreignKey: { + name: 'ownerId', + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + }) + declare drivingLicense?: NonAttribute; +} +``` + +The possible choices are `RESTRICT`, `CASCADE`, `NO ACTION`, `SET DEFAULT` and `SET NULL`. + +By default, Sequelize will set the following values: + +- for `ON DELETE`: `SET NULL` if the foreign key is nullable, and `CASCADE` if it is not. +- for `ON UPDATE`: `CASCADE` diff --git a/docs/associations/has-many.md b/docs/associations/has-many.md new file mode 100644 index 00000000..8b761c60 --- /dev/null +++ b/docs/associations/has-many.md @@ -0,0 +1,431 @@ +--- +sidebar_position: 3 +title: HasMany +--- + +# The HasMany Association + +The HasMany association is used to create a One-To-Many relationship between two models. + +In a One-To-Many relationship, a row of one table is associated with _zero, one or more_ rows of another table. + +For instance, a post can have zero or more comments, but a comment can only belong to one post. + +```mermaid +erDiagram + posts ||--o{ comments : comments +``` + +## Defining the Association + +Here is how you would define the `Post` and `Comment` models in Sequelize: + +```ts +import { Model, DataTypes, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from '@sequelize/core'; +import { PrimaryKey, Attribute, AutoIncrement, NotNull, HasMany, BelongsTo } from '@sequelize/core/decorators-legacy'; + +class Post extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + @HasMany(() => Comment, /* foreign key */ 'postId') + declare comments?: NonAttribute; + // highlight-end +} + +class Comment extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + // This is the foreign key + @Attribute(DataTypes.INTEGER) + @NotNull + declare postId: number; + // highlight-end +} +``` + +Note that in the example above, the `Comment` model has a foreign key to the `Post` model. __`HasMany` adds the foreign key +on the model the association targets.__ + +## Inverse association + +The `HasMany` association automatically creates an inverse association on the target model. +The inverse association is a [`BelongsTo`](./belongs-to.md) association. + +You can configure that inverse association by using the `inverse` option: + +```ts +import { Model, DataTypes, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from '@sequelize/core'; +import { PrimaryKey, Attribute, AutoIncrement, NotNull, HasMany, BelongsTo } from '@sequelize/core/decorators-legacy'; + +class Post extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @HasMany(() => Comment, { + foreignKey: 'postId', + // highlight-start + inverse: { + as: 'post', + }, + // highlight-end + }) + declare comments?: NonAttribute; +} + +class Comment extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + /** Defined by {@link Post.comments} */ + declare post?: NonAttribute; + // highlight-end + + // This is the foreign key + @Attribute(DataTypes.INTEGER) + @NotNull + declare postId: number; +} +``` + +## Association Methods + +All associations add methods to the source model[^1]. 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`: + +```ts +import { HasManyGetAssociationsMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare getComments: HasManyGetAssociationsMixin; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); + +// highlight-start +const comments: Comment[] = await post.getComments(); +// highlight-end +``` + +### Association Setter (`setX`) + +The association setter is used to set the associated models. It is always named `set`. + +If the model is already associated to one or more models, the old associations are removed before the new ones are added. + +```ts +import { HasManySetAssociationsMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare setComments: HasManySetAssociationsMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); +const [comment1, comment2, comment3] = await Comment.findAll({ limit: 3 }); + +// highlight-start +// Remove all previous associations and set the new ones +await post.setComments([comment1, comment2, comment3]); + +// 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 post.setComments([1, 2, 3]); +// highlight-end +``` + +:::caution + +If the foreign key is not nullable, calling this method will delete the previously associated models (if any), +as setting their foreign key to `null` would result in a validation error. + +If the foreign key is nullable, it will by default set it to null on all previously associated models. +You can use the `destroyPrevious` option to delete the previously associated models instead: + +```ts +// this will delete all previously associated models +await post.setComments([], { destroyPrevious: true }); +``` + +::: + +### 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`: Associates a single new model. +- `add`: Associates multiple new models. + +```ts +import { HasManyAddAssociationMixin, HasManyAddAssociationsMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare addComment: HasManyAddAssociationMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + + declare addComments: HasManyAddAssociationsMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); +const [comment1, comment2, comment3] = await Comment.findAll({ limit: 3 }); + +// highlight-start +// Add a single comment, without removing existing ones +await post.addComment(comment1); + +// Add multiple comments, without removing existing ones +await post.addComments([comment1, comment2]); + +// 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 post.addComment(1); +await post.addComments([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`: Removes a single associated model. +- `remove`: Removes multiple associated models. + + +```ts +import { HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare removeComment: HasManyRemoveAssociationMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + + declare removeComments: HasManyRemoveAssociationsMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); +const [comment1, comment2, comment3] = await Comment.findAll({ limit: 3 }); + +// highlight-start +// Remove a single comment, without removing existing ones +await post.removeComment(comment1); + +// Remove multiple comments, without removing existing ones +await post.removeComments([comment1, comment2]); + +// 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 post.removeComment(1); +await post.removeComments([1, 2, 3]); +// highlight-end +``` + +:::caution + +If the foreign key is not nullable, calling this method will delete the specified models, +as setting their foreign key to `null` would result in a validation error. + +If the foreign key is nullable, it will by default set it to null on all specified models. +You can use the `destroy` option to delete the previously associated models instead: + +```ts +// this will delete comments with PKs 1, 2 and 3 +await post.removeComments([1, 2, 3], { destroy: true }); +``` + +::: + +### 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`. + +```ts +import { HasManyCreateAssociationMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare createComment: HasManyCreateAssociationMixin; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); + +// highlight-start +const comment = await post.createComment({ + content: 'This is a comment', +}); +// 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 `HasManyCreateAssociationMixin` type. + +```ts +HasManyCreateAssociationMixin + ^ Here +``` + +::: + +### Association Checker (`hasX`) + +The association checker is used to check if a model is associated with another model. It has two versions: + +- `has`: Checks if a single model is associated. +- `has`: Checks whether all the specified models are associated. + +```ts +import { HasManyHasAssociationMixin, HasManyHasAssociationsMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare hasComment: HasManyHasAssociationMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + + declare hasComments: HasManyHasAssociationsMixin< + Comment, + /* this is the type of the primary key of the target */ + Comment['id'] + >; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); + +// highlight-start +// Returns true if the post has a comment with id 1 +const isAssociated = await post.hasComment(comment1); + +// Returns true if the post is associated to all specified comments +const isAssociated = await post.hasComments([comment1, comment2, comment3]); + +// Like other association methods, you can also use the primary key of the associated model as a way to identify it +const isAssociated = await post.hasComments([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`. + +```ts +import { HasManyCountAssociationsMixin } from '@sequelize/core'; + +class Post extends Model, InferCreationAttributes> { + @HasMany(() => Comment, 'postId') + declare comments?: NonAttribute; + + // highlight-start + declare countComments: HasManyCountAssociationsMixin; + // highlight-end +} + +// ... + +const post = await Post.findByPk(1); + +// highlight-start +// Returns the number of associated comments +const count = await post.countComments(); +// highlight-end +``` + +## Foreign Key targets (`sourceKey`) + +By default, Sequelize will use the primary key of the source model as the attribute the foreign key references. +You can customize this by using the `sourceKey` option. + +```ts +class Post extends Model { + declare id: CreationOptional; + + @HasMany(() => Comment, { + foreignKey: 'postId', + // highlight-next-line + // The foreign key will reference the `id` attribute of the `Post` model + sourceKey: 'id', + }) + declare comments?: NonAttribute; +} +``` + +[^1]: The source model is the model that defines the association. diff --git a/docs/associations/has-one.md b/docs/associations/has-one.md new file mode 100644 index 00000000..488ddc49 --- /dev/null +++ b/docs/associations/has-one.md @@ -0,0 +1,289 @@ +--- +sidebar_position: 2 +title: HasOne +--- + +# The HasOne Association + +The HasOne association is used to create a One-To-One relationship between two models. + +In a One-To-One relationship, a row of one table is associated with a single row of another table. + +The most common type of One-To-One relationship is one where one side is mandatory, and the other side is optional. +For instance, a driving license always belongs to a single person, but a person can have zero or one driving licenses (from the same place). + +```mermaid +erDiagram + people ||--o| driving_licenses : drivingLicense +``` + +## Defining the Association + +Here is how you would define the `Person` and `DrivingLicense` models in Sequelize: + +```ts +import { Model, DataTypes, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from '@sequelize/core'; +import { PrimaryKey, Attribute, AutoIncrement, NotNull, HasOne, BelongsTo } from '@sequelize/core/decorators-legacy'; + +class Person extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + @HasOne(() => DrivingLicense, /* foreign key */ 'ownerId') + declare drivingLicense?: NonAttribute; + // highlight-end +} + +class DrivingLicense extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + // This is the foreign key + @Attribute(DataTypes.INTEGER) + @NotNull + declare ownerId: number; + // highlight-end +} +``` + +Note that in the example above, the `DrivingLicense` model has a foreign key to the `Person` model. __`HasOne` adds the foreign key +on the model the association targets.__ + +Always think about which model should own the foreign key, as the foreign key is the one that enforces the relationship. + +In this case, because the driving license model has a non-null foreign key, +it is impossible to create a Driving License without assigning it to a Person. +However, it's possible to create a Person without a Driving License. + +If you made the foreign key nullable, the relationship would be optional on both sides. + +:::info Unique FK + +When using `HasOne`, you may want to add a unique constraint on the foreign key to ensure that only one row can be associated with the source row. + +You can do this by using the `@Unique` decorator on the foreign key: + +```ts +class DrivingLicense extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @NotNull + // highlight-next-line + @Unique + declare ownerId: number; +} +``` + +::: + +## Inverse association + +The `HasOne` association automatically creates an inverse association on the target model. +The inverse association is a [`BelongsTo`](./belongs-to.md) association. + +You can configure that inverse association by using the `inverse` option: + +```ts +import { Model, DataTypes, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from '@sequelize/core'; +import { PrimaryKey, Attribute, AutoIncrement, NotNull, HasOne, BelongsTo } from '@sequelize/core/decorators-legacy'; + +class Person extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @HasOne(() => DrivingLicense, { + foreignKey: 'ownerId', + // highlight-start + inverse: { + as: 'owner', + }, + // highlight-end + }) + declare drivingLicense?: NonAttribute; +} + +class DrivingLicense extends Model, InferCreationAttributes> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + // highlight-start + /** Defined by {@link Person.drivingLicense} */ + declare owner?: NonAttribute; + // highlight-end + + @Attribute(DataTypes.INTEGER) + @NotNull + declare ownerId: number; +} +``` + +## Association Methods + +All associations add methods to the source model[^1]. 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 model. It is always named `get`: + +```ts +import { HasOneGetAssociationMixin } from '@sequelize/core'; + +class Person extends Model, InferCreationAttributes> { + @HasOne(() => DrivingLicense, 'ownerId') + declare drivingLicense?: NonAttribute; + + // highlight-start + declare getDrivingLicense: HasOneGetAssociationMixin; + // highlight-end +} + +// ... + +const person = await Person.findByPk(1); + +// highlight-start +const drivingLicense: DrivingLicense | null = await person.getDrivingLicense(); +// highlight-end +``` + +### Association Setter (`setX`) + +The association setter is used to set (or unset) which model is associated to this one. It is always named `set`. +It accepts a single model instance, or `null` to unset the association. + +```ts +import { HasOneSetAssociationMixin } from '@sequelize/core'; + +class Person extends Model, InferCreationAttributes> { + @HasOne(() => DrivingLicense, 'ownerId') + declare drivingLicense?: NonAttribute; + + // highlight-start + declare setDrivingLicense: HasOneSetAssociationMixin< + DrivingLicense, + /* this is the type of the primary key of the target */ + DrivingLicense['id'] + >; + // highlight-end +} + +// ... + +const person = await Person.findByPk(1); +const drivingLicense = await DrivingLicense.create({ /* ... */ }); + +// highlight-start +// Note: If the driving license already has an owner, it will be replaced by the new owner. +await person.setDrivingLicense(drivingLicense); + +// 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 person.setDrivingLicense(5); + +// Removes the driving license from the person +await person.setDrivingLicense(null); +// highlight-end +``` + +:::caution + +If the foreign key is not nullable, calling this method will delete the previously associated model (if any), +as setting its foreign key to `null` would result in a validation error. + +If the foreign key is nullable, it will by default set it to null. You can use the `destroyPrevious` option to delete +the previously associated model instead: + +```ts +await person.setDrivingLicense(newDrivingLicense, { destroyPrevious: true }); +``` + +::: + +### Association Creator (`createX`) + +The association creator is used to create a new associated model. It is always named `create`. +It accepts the same arguments as the associated model's `create` method. + +```ts +import { HasOneCreateAssociationMixin } from '@sequelize/core'; + +class Person extends Model, InferCreationAttributes> { + @HasOne(() => DrivingLicense, 'ownerId') + declare drivingLicense?: NonAttribute; + + // highlight-start + declare createDrivingLicense: HasOneCreateAssociationMixin; + // highlight-end +} + +// ... + +const person = await Person.findByPk(1); + +// highlight-start +const drivingLicense: DrivingLicense = await person.createDrivingLicense({ + number: '123456789', +}); +// highlight-end +``` + +:::info Omitting the foreign key + +In the example above, we did not need to specify the `ownerId` 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 `HasOneCreateAssociationMixin` type. + +```ts +HasOneCreateAssociationMixin + ^ Here +``` + +::: + +### Inverse Association Methods + +The [inverse association](#inverse-association) also adds methods to its source model. See [the `BelongsTo` documentation](./belongs-to.md#association-methods) for the list of methods. + +## Making the association mandatory on both sides + +In theory, a One-To-One relationship can also be mandatory on both sides. +For instance, a person always has a birth certificate, and a birth certificate always belongs to a person. + +```mermaid +erDiagram + people ||--|| birth_certificates : birthCertificate +``` + +In practice however, it is impossible to enforce this relationship in a database. You need to handle this in your business logic. + +## Foreign Key targets (`sourceKey`) + +By default, Sequelize will use the primary key of the source model as the attribute the foreign key references. +You can customize this by using the `sourceKey` option. + +```ts +class Person extends Model { + declare id: CreationOptional; + + @HasOne(() => DrivingLicense, { + foreignKey: 'ownerId', + // highlight-next-line + // The foreign key will reference the `id` attribute of the `Person` model + sourceKey: 'id', + }) + declare drivingLicense?: NonAttribute; +} +``` + +[^1]: The source model is the model that defines the association. diff --git a/docs/associations/polymorphic-associations.md b/docs/associations/polymorphic-associations.md new file mode 100644 index 00000000..402ae63a --- /dev/null +++ b/docs/associations/polymorphic-associations.md @@ -0,0 +1,239 @@ +--- +title: Polymorphic Associations +--- + +A **polymorphic association** is an association that can target multiple models. For example, imagine a `Comment` model that can belong to either a `Article` or a `Video`. + +Sequelize offers three ways of implementing polymorphic associations, in order of recommendation: + +- A. Using [Model Inheritance](#inheritance-based-polymorphic-associations) (recommended) + - 👍 This solution supports foreign keys + - 👍 Tables are lighter, more performant + - 👍 Can easily add model-specific attributes + - ❌ This solution requires more tables +- B. Using a [single model with multiple foreign keys](#single-model-multiple-foreign-key-polymorphic-associations) + - 👍 This solution supports foreign keys + - 👍 Uses a single table +- C. Using a [single model with a single foreign key](#single-model-single-foreign-key-polymorphic-associations) + - 👍 Uses a single table + - ❌ Does not support foreign keys + +## Inheritance-based polymorphic associations + +The way this polymorphic association works is by creating a base model, such as `AbstractComment`, +which defines the common fields between all comments. +Then, we create models that [inherit](../models/inheritance.md) from it +for each model that can have comments, such as `ArticleComment` and `VideoComment`. + +```ts +// This is the base model, which defines the common fields between all comments. +@AbstractModel +abstract class AbstractComment extends Model { + declare id: number; + + @Attributes(DataTypes.STRING) + @NotNull + declare content: string; + + @Attributes(DataTypes.INTEGER) + @NotNull + declare targetId: number; +} + +// This is the model for comments on articles. +class ArticleComment extends AbstractComment, InferCreationAttributes> { + @BelongsTo(() => Article, 'targetId') + declare target?: Article; +} + +// This is the model for comments on videos. +class VideoComment extends AbstractComment, InferCreationAttributes> { + @BelongsTo(() => Video, 'targetId') + declare target?: Video; +} +``` + +The above code will create two tables: `ArticleComments` and `VideoComments`. + +## Single-model, multiple-foreign-key polymorphic associations + +This solution only requires a single table, to which we add multiple, mutually-exclusive foreign keys: + +```ts +class Comment extends Model, InferCreationAttributes> { + declare id: number; + + @Attributes(DataTypes.STRING) + @NotNull + declare content: string; + + @Attributes(DataTypes.INTEGER) + declare articleId: number | null; + + @BelongsTo(() => Article, 'articleId') + declare article?: Article; + + @Attributes(DataTypes.INTEGER) + declare videoId: number | null; + + @BelongsTo(() => Video, 'videoId') + declare video?: Video; +} +``` + +You can then determine which foreign key to use by checking which one is `null`. + +We recommend that you add a [`CHECK` constraint](../models/validations-and-constraints.md#check-constraints) on this table to ensure that only one of the foreign keys is not null at a time. + +## Single-model, single-foreign-key polymorphic associations + +:::caution + +This type of polymorphic associations cannot use foreign keys, as a single column can only ever reference one other table. +This may be a problem if you want to use foreign keys for data integrity, as well as for `SELECT` performance. + +Due to using the same column for multiple associations, you also put yourself at greater risk of creating a data integrity issue. + +For these reasons, we highly recommend using one of the other two solutions instead. Proceed with caution. + +::: + +In this type of polymorphic association, we don't use foreign keys at all. +Instead, we use two columns: one to store the type of the associated model, and one to store the ID of the associated model. + +As stated above, we must disable the foreign key constraints on the association, as the same column is referencing multiple tables. +This can be done by using the `constraints: false`. + +We then use [association scopes](./association-scopes.md) to filter which comments belong to which models. + +```ts +class Comment extends Model, InferCreationAttributes> { + declare id: number; + + @Attributes(DataTypes.STRING) + @NotNull + declare content: string; + + @Attributes(DataTypes.STRING) + @NotNull + declare targetModel: 'article' | 'video'; + + @Attributes(DataTypes.INTEGER) + @NotNull + declare targetId: number; + + /** Defined by {@link Article#comments} */ + declare article?: NonAttribute
; + + /** Defined by {@link Video#comments} */ + declare video?: NonAttribute