跳至主要內容
版本:v6 - stable (穩定版)

範圍 (Scopes)

範圍 (Scopes) 用於幫助您重複使用程式碼。您可以定義常用的查詢,並指定諸如 whereincludelimit 等選項。

本指南涉及模型範圍 (model scopes)。您可能也會對關聯範圍 (association scopes) 的指南感興趣,它們類似但不相同。

定義

範圍 (Scopes) 在模型定義中定義,可以是尋找器 (finder) 物件,或是回傳尋找器物件的函式 - 除了預設範圍 (default scope),它只能是一個物件

class Project extends Model {}
Project.init(
{
// Attributes
},
{
defaultScope: {
where: {
active: true,
},
},
scopes: {
deleted: {
where: {
deleted: true,
},
},
activeUsers: {
include: [{ model: User, where: { active: true } }],
},
random() {
return {
where: {
someNumber: Math.random(),
},
};
},
accessLevel(value) {
return {
where: {
accessLevel: {
[Op.gte]: value,
},
},
};
},
sequelize,
modelName: 'project',
},
},
);

您也可以在模型定義後,透過呼叫 YourModel.addScope 來新增範圍。當 include 中的模型可能在定義其他模型時尚未定義時,這特別有用。

預設範圍 (default scope) 總是會被套用。這表示,以上述模型定義為例,Project.findAll() 將會建立以下查詢

SELECT * FROM projects WHERE active = true

預設範圍 (default scope) 可以透過呼叫 .unscoped().scope(null) 或透過呼叫另一個範圍來移除。

await Project.scope('deleted').findAll(); // Removes the default scope
SELECT * FROM projects WHERE deleted = true

在範圍定義中包含已設定範圍的模型也是可行的。這允許您避免重複定義 includeattributeswhere 定義。以上述範例來說,在被包含的 User 模型上呼叫 active 範圍 (而不是直接在 include 物件中指定條件)

// The `activeUsers` scope defined in the example above could also have been defined this way:
Project.addScope('activeUsers', {
include: [{ model: User.scope('active') }],
});

用法

範圍 (Scopes) 是透過在模型定義上呼叫 .scope 來套用,並傳遞一個或多個範圍的名稱。.scope 會回傳一個功能完整的模型實例,它具有所有常規方法:.findAll.update.count.destroy 等。您可以儲存這個模型實例並稍後重複使用。

const DeletedProjects = Project.scope('deleted');
await DeletedProjects.findAll();

// The above is equivalent to:
await Project.findAll({
where: {
deleted: true,
},
});

範圍 (Scopes) 適用於 .find.findAll.count.update.increment.destroy

身為函式的範圍 (Scopes) 可以用兩種方式呼叫。如果範圍不接受任何參數,則可以像平常一樣呼叫。如果範圍接受參數,則傳遞一個物件

await Project.scope('random', { method: ['accessLevel', 19] }).findAll();

產生的 SQL

SELECT * FROM projects WHERE someNumber = 42 AND accessLevel >= 19

合併

可以透過將範圍陣列傳遞給 .scope,或將範圍作為連續的參數傳遞,同時套用多個範圍。

// These two are equivalent
await Project.scope('deleted', 'activeUsers').findAll();
await Project.scope(['deleted', 'activeUsers']).findAll();

產生的 SQL

SELECT * FROM projects
INNER JOIN users ON projects.userId = users.id
WHERE projects.deleted = true
AND users.active = true

如果您想在預設範圍 (default scope) 旁邊套用另一個範圍,請將 defaultScope 鍵傳遞給 .scope

await Project.scope('defaultScope', 'deleted').findAll();

產生的 SQL

SELECT * FROM projects WHERE active = true AND deleted = true

當呼叫多個範圍時,後續範圍的鍵將會覆寫先前的鍵 (類似於 Object.assign),但 whereinclude 除外,它們將會被合併。考慮以下兩個範圍

YourModel.addScope('scope1', {
where: {
firstName: 'bob',
age: {
[Op.gt]: 20,
},
},
limit: 2,
});
YourModel.addScope('scope2', {
where: {
age: {
[Op.lt]: 30,
},
},
limit: 10,
});

使用 .scope('scope1', 'scope2') 將會產生以下的 WHERE 子句

WHERE firstName = 'bob' AND age < 30 LIMIT 10

請注意 limitage 如何被 scope2 覆寫,而 firstName 則被保留。limitoffsetorderparanoidlockraw 欄位會被覆寫,而 where 預設會淺層合併 (shallowly merged) (表示相同的鍵會被覆寫)。如果 whereMergeStrategy 旗標設定為 and (在模型上或在 sequelize 實例上),where 欄位將會使用 and 運算符合併。

例如,如果 YourModel 初始化如下

YourModel.init(
{
/* attributes */
},
{
// ... other init options
whereMergeStrategy: 'and',
},
);

使用 .scope('scope1', 'scope2') 將會產生以下的 WHERE 子句

WHERE firstName = 'bob' AND age > 20 AND age < 30 LIMIT 10

請注意,多個套用的範圍的 attributes 鍵會以 attributes.exclude 永遠被保留的方式合併。這允許合併多個範圍,且永遠不會在最終範圍中洩漏敏感欄位。

當將 find 物件直接傳遞給已設定範圍模型的 findAll (和類似的尋找器) 時,也會套用相同的合併邏輯

Project.scope('deleted').findAll({
where: {
firstName: 'john',
},
});

產生的 where 子句

WHERE deleted = true AND firstName = 'john'

這裡的 deleted 範圍與尋找器合併。如果我們將 where: { firstName: 'john', deleted: false } 傳遞給尋找器,則 deleted 範圍將被覆寫。

合併 includes

Includes 會根據被包含的模型遞迴地合併。這是一個非常強大的合併功能,在 v5 中新增,透過範例更容易理解。

考慮模型 FooBarBazQux,它們具有以下一對多關聯

const Foo = sequelize.define('Foo', { name: Sequelize.STRING });
const Bar = sequelize.define('Bar', { name: Sequelize.STRING });
const Baz = sequelize.define('Baz', { name: Sequelize.STRING });
const Qux = sequelize.define('Qux', { name: Sequelize.STRING });
Foo.hasMany(Bar, { foreignKey: 'fooId' });
Bar.hasMany(Baz, { foreignKey: 'barId' });
Baz.hasMany(Qux, { foreignKey: 'bazId' });

現在,考慮在 Foo 上定義的以下四個範圍

Foo.addScope('includeEverything', {
include: {
model: Bar,
include: [
{
model: Baz,
include: Qux,
},
],
},
});

Foo.addScope('limitedBars', {
include: [
{
model: Bar,
limit: 2,
},
],
});

Foo.addScope('limitedBazs', {
include: [
{
model: Bar,
include: [
{
model: Baz,
limit: 2,
},
],
},
],
});

Foo.addScope('excludeBazName', {
include: [
{
model: Bar,
include: [
{
model: Baz,
attributes: {
exclude: ['name'],
},
},
],
},
],
});

這四個範圍可以輕鬆地深度合併,例如,透過呼叫 Foo.scope('includeEverything', 'limitedBars', 'limitedBazs', 'excludeBazName').findAll(),這完全等同於呼叫以下內容

await Foo.findAll({
include: {
model: Bar,
limit: 2,
include: [
{
model: Baz,
limit: 2,
attributes: {
exclude: ['name'],
},
include: Qux,
},
],
},
});

// The above is equivalent to:
await Foo.scope(['includeEverything', 'limitedBars', 'limitedBazs', 'excludeBazName']).findAll();

觀察這四個範圍如何合併為一個。範圍的 includes 是根據被包含的模型合併的。如果一個範圍包含模型 A,而另一個範圍包含模型 B,則合併結果將會包含模型 A 和模型 B。另一方面,如果兩個範圍都包含相同的模型 A,但具有不同的選項 (例如巢狀 includes 或其他屬性),則這些選項將會遞迴地合併,如上所示。

上述說明的合併方式,無論套用範圍的順序如何,都以完全相同的方式運作。只有當某個選項被兩個不同的範圍設定時,順序才會有所不同 - 但上述範例並非如此,因為每個範圍都做不同的事情。

此合併策略也以完全相同的方式適用於傳遞給 .findAll.findOne 等的選項。