範圍 (Scopes)
範圍 (Scopes) 用於幫助您重複使用程式碼。您可以定義常用的查詢,並指定諸如 where
、include
、limit
等選項。
本指南涉及模型範圍 (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
在範圍定義中包含已設定範圍的模型也是可行的。這允許您避免重複定義 include
、attributes
或 where
定義。以上述範例來說,在被包含的 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),但 where
和 include
除外,它們將會被合併。考慮以下兩個範圍
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
請注意 limit
和 age
如何被 scope2
覆寫,而 firstName
則被保留。limit
、offset
、order
、paranoid
、lock
和 raw
欄位會被覆寫,而 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 中新增,透過範例更容易理解。
考慮模型 Foo
、Bar
、Baz
和 Qux
,它們具有以下一對多關聯
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
等的選項。