Eager Loading
アソシエーションガイドで簡単に説明したように、Eager Loadingとは、複数のモデルのデータを一度にクエリする行為です(1つの「メイン」モデルと1つ以上の関連付けられたモデル)。SQLレベルでは、これは1つ以上のJOINを使用したクエリです。
これを実行すると、関連付けられたモデルは、Sequelizeによって、返されるオブジェクト内の適切に名前付けされた、自動的に作成されたフィールドに追加されます。
Sequelizeでは、Eager Loadingは主に、モデルファインダークエリ(findOne、findAllなど)のincludeオプションを使用して実行されます。
基本例
次の設定を想定します
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' });
関連付けられた単一要素の取得
まず、関連付けられたユーザーを含むすべてのタスクを読み込んでみましょう
const tasks = await Task.findAll({ include: User });
console.log(JSON.stringify(tasks, null, 2));
出力
[
  {
    "name": "A Task",
    "id": 1,
    "userId": 1,
    "user": {
      "name": "John Doe",
      "id": 1
    }
  }
]
ここで、tasks[0].user instanceof Userはtrueです。これは、Sequelizeが関連付けられたモデルを取得すると、それらがモデルインスタンスとして出力オブジェクトに追加されることを示しています。
上記では、関連付けられたモデルは、取得されたタスクのuserという新しいフィールドに追加されました。このフィールドの名前は、関連付けられたモデルの名前(アソシエーションがhasManyまたはbelongsToManyの場合、その複数形が使用されます)に基づいて、Sequelizeによって自動的に選択されました。言い換えれば、Task.belongsTo(User)なので、タスクは1人のユーザーに関連付けられているため、論理的な選択は単数形です(Sequelizeは自動的にこれに従います)。
関連付けられたすべての要素の取得
今度は、特定のタスクに関連付けられているユーザーを読み込む代わりに、逆を行います。つまり、特定のユーザーに関連付けられているすべてのタスクを見つけます。
メソッド呼び出しは基本的に同じです。唯一の違いは、クエリ結果で作成された追加フィールドが複数形(この場合はtasks)を使用し、その値がタスクインスタンスの配列(上記のように単一のインスタンスではなく)であることです。
const users = await User.findAll({ include: Task });
console.log(JSON.stringify(users, null, 2));
出力
[
  {
    "name": "John Doe",
    "id": 1,
    "tasks": [
      {
        "name": "A Task",
        "id": 1,
        "userId": 1
      }
    ]
  }
]
アソシエーションが1対多であるため、アクセサ(結果のインスタンスのtasksプロパティ)は複数形であることに注意してください。
エイリアスされたアソシエーションの取得
アソシエーションにエイリアスが付けられている場合(asオプションを使用)、モデルを含める際にこのエイリアスを指定する必要があります。モデルをincludeオプションに直接渡す代わりに、modelとasの2つのオプションを持つオブジェクトを提供する必要があります。
上記のユーザーのToolはInstrumentsとしてエイリアスされていることに注意してください。正しく取得するには、ロードするモデルとエイリアスの両方を指定する必要があります。
const users = await User.findAll({
  include: { model: Tool, as: 'Instruments' },
});
console.log(JSON.stringify(users, null, 2));
出力
[
  {
    "name": "John Doe",
    "id": 1,
    "Instruments": [
      {
        "name": "Scissor",
        "id": 1,
        "userId": 1
      }
    ]
  }
]
アソシエーションのエイリアス名と一致する文字列を指定することで、エイリアス名で含めることもできます。
User.findAll({ include: 'Instruments' }); // Also works
User.findAll({ include: { association: 'Instruments' } }); // Also works
必須Eager Loading
Eager Loadingでは、関連付けられたモデルを持つレコードのみを返すようにクエリを強制し、クエリをデフォルトのOUTER JOINからINNER JOINに効果的に変換できます。これは、次のようにrequired: trueオプションを使用することで実行されます。
User.findAll({
  include: {
    model: Task,
    required: true,
  },
});
このオプションは、ネストされたincludeでも機能します。
関連付けられたモデルレベルでフィルタリングされたEager Loading
Eager Loadingでは、次の例のようにwhereオプションを使用して関連付けられたモデルをフィルタリングすることもできます。
User.findAll({
  include: {
    model: Tool,
    as: 'Instruments',
    where: {
      size: {
        [Op.ne]: 'small',
      },
    },
  },
});
生成された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';
上記の生成されたSQLクエリは、少なくとも1つの条件に一致するツールを持つユーザーのみを取得することに注意してください(この場合はsmallではない)。これは、whereオプションがinclude内で使用されると、Sequelizeが自動的にrequiredオプションをtrueに設定するためです。つまり、OUTER JOINの代わりにINNER JOINが行われ、少なくとも1つの一致する子を持つ親モデルのみが返されます。
また、使用されたwhereオプションは、INNER JOINのON句の条件に変換されたことにも注意してください。ON句ではなく、トップレベルのWHERE句を取得するには、別の方法を行う必要があります。これは次に示します。
他の列への参照
関連付けられたモデルの値を参照するincludeされたモデルにWHERE句を適用したい場合は、次の例に示すようにSequelize.col関数を使用するだけで済みます。
// Find all projects with a least one task where task.state === project.state
Project.findAll({
  include: {
    model: Task,
    where: {
      state: Sequelize.col('project.state'),
    },
  },
});
トップレベルの複雑なWHERE句
ネストされた列を含むトップレベルのWHERE句を取得するために、Sequelizeはネストされた列を参照する方法を提供します。それは'$nested.column$'構文です。
これは、たとえば、includeされたモデルのwhere条件をON条件からトップレベルのWHERE句に移動するために使用できます。
User.findAll({
  where: {
    '$Instruments.size$': { [Op.ne]: 'small' },
  },
  include: [
    {
      model: Tool,
      as: 'Instruments',
    },
  ],
});
生成された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';
$nested.column$構文は、$some.super.deeply.nested.column$など、複数レベルにネストされた列にも機能します。したがって、これを使用して、深くネストされた列に複雑なフィルタを適用できます。
requiredオプションの有無にかかわらず、include内で使用される内部whereオプションと、$nested.column$構文を使用したトップレベルのwhereとのすべての違いをよりよく理解するために、以下に4つの例を示します。
// 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,
  },
});
順番に生成された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';
RIGHT OUTER JOINを使用したフェッチ(MySQL、MariaDB、PostgreSQL、MSSQLのみ)
デフォルトでは、アソシエーションはLEFT OUTER JOINを使用してロードされます。つまり、親テーブルのレコードのみが含まれます。使用しているDialectがサポートしている場合、rightオプションを渡すことで、この動作をRIGHT OUTER JOINに変更できます。
現在、SQLiteはライトジョインをサポートしていません。
注記:rightは、requiredがfalseの場合にのみ尊重されます。
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
  }]
});
複数Eager Loading
includeオプションは、複数の関連付けられたモデルを一度にフェッチするために配列を受け取ることができます。
Foo.findAll({
  include: [
    {
      model: Bar,
      required: true
    },
    {
      model: Baz,
      where: /* ... */
    },
    Qux // Shorthand syntax for { model: Qux } also works here
  ]
})
多対多リレーションシップでのEager Loading
多対多リレーションシップを持つモデルでEager Loadingを実行すると、デフォルトでSequelizeはジャンクションテーブルデータも取得します。例えば
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));
出力
{
  "id": 1,
  "name": "foo",
  "Bars": [
    {
      "id": 1,
      "name": "bar",
      "Foo_Bar": {
        "FooId": 1,
        "BarId": 1
      }
    }
  ]
}
Eager Loadingされたすべてのbarインスタンスの"Bars"プロパティには、関連するSequelizeのジャンクションモデルインスタンスであるFoo_Barという追加のプロパティがあることに注意してください。デフォルトでは、Sequelizeはこの追加のプロパティを作成するために、ジャンクションテーブルからすべての属性を取得します。
ただし、取得する属性を指定できます。これは、includeのthroughオプションに適用されたattributesオプションを使用して行います。例えば
Foo.findAll({
  include: [
    {
      model: Bar,
      through: {
        attributes: [
          /* list the wanted attributes here */
        ],
      },
    },
  ],
});
ジャンクションテーブルから何も取得したくない場合は、includeオプションのthroughオプション内のattributesオプションに空の配列を明示的に指定できます。この場合、何も取得されず、追加のプロパティは作成されません。
Foo.findOne({
  include: {
    model: Bar,
    through: {
      attributes: [],
    },
  },
});
出力
{
  "id": 1,
  "name": "foo",
  "Bars": [
    {
      "id": 1,
      "name": "bar"
    }
  ]
}
多対多リレーションシップからモデルを含める際は、ジャンクションテーブルにフィルターを適用することもできます。これは、include オプション内のwhereオプションを使用して行います。例:
User.findAll({
  include: [
    {
      model: Project,
      through: {
        where: {
          // Here, `completed` is a column present at the junction table
          completed: true,
        },
      },
    },
  ],
});
生成されたSQL (SQLite使用)
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;
すべてを含める
関連付けられたすべてのモデルを含めるには、allおよびnestedオプションを使用できます。
// 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 } });
ソフト削除されたレコードを含める
ソフト削除されたレコードを eager load したい場合は、include.paranoidをfalseに設定します。
User.findAll({
  include: [
    {
      model: Tool,
      as: 'Instruments',
      where: { size: { [Op.ne]: 'small' } },
      paranoid: false,
    },
  ],
});
eager loadされた関連付けのソート
eager loadされたモデルにORDER句を適用する場合は、ソートしたいネストされたモデルの指定から始まる拡張配列を使用して、最上位レベルのorderオプションを使用する必要があります。
これは、例で理解しやすくなります。
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'],
  ],
});
多対多リレーションシップの場合、throughテーブルの属性でソートすることもできます。たとえば、DivisionとDepartmentの間にジャンクションモデルがDepartmentDivisionである多対多リレーションシップがあると仮定すると、次のようにできます。
Company.findAll({
  include: {
    model: Division,
    include: Department,
  },
  order: [[Division, DepartmentDivision, 'name', 'ASC']],
});
上記のすべての例で、orderオプションが最上位レベルで使用されていることに気付かれたと思います。includeオプション内でorderが機能する唯一の状況は、separate: trueが使用されている場合です。その場合、使い方は次のとおりです。
// 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']],
  },
});
サブクエリを含む複雑なソート
より複雑なソートを支援するためのサブクエリの使い方の例については、サブクエリに関するガイドを参照してください。
ネストされた eager loading
ネストされた eager loadingを使用して、関連モデルのすべての関連モデルをロードできます。
const users = await User.findAll({
  include: {
    model: Tool,
    as: 'Instruments',
    include: {
      model: Teacher,
      include: [
        /* etc */
      ],
    },
  },
});
console.log(JSON.stringify(users, null, 2));
出力
[
  {
    "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"
        }
      }
    ]
  }
]
これにより、外部結合が生成されます。ただし、関連モデルのwhere句は内部結合を作成し、一致するサブモデルを持つインスタンスのみを返します。すべての親インスタンスを返すには、required: falseを追加する必要があります。
User.findAll({
  include: [
    {
      model: Tool,
      as: 'Instruments',
      include: [
        {
          model: Teacher,
          where: {
            school: 'Woodstock Music School',
          },
          required: false,
        },
      ],
    },
  ],
});
上記のクエリは、すべてのユーザーとその楽器をすべて返しますが、「Woodstock Music School」に関連付けられている教師のみを返します。
includeを使用したfindAndCountAllの使用
findAndCountAllユーティリティ関数はincludeをサポートします。requiredとしてマークされたincludeのみがcountで考慮されます。たとえば、プロフィールを持つすべてのユーザーを見つけてカウントしたい場合
User.findAndCountAll({
  include: [{ model: Profile, required: true }],
  limit: 3,
});
Profileのincludeにrequiredが設定されているため、内部結合になり、プロフィールを持つユーザーのみがカウントされます。includeからrequiredを削除すると、プロフィールのあるユーザーとないユーザーの両方がカウントされます。includeにwhere句を追加すると、自動的にrequiredがtrueになります。
User.findAndCountAll({
  include: [{ model: Profile, where: { active: true } }],
  limit: 3,
});
上記のクエリは、アクティブなプロフィールを持つユーザーのみをカウントします。これは、includeにwhere句を追加すると、requiredが暗黙的にtrueに設定されるためです。