Sequelize Scopes 部分源码解析

前言

在使用SequelizeScopes的时候,遇到了一些坑,也很好奇里面怎样实现的,所以特意看一遍源码~~

定义

定义方式有两种:

第一种

Sequelize.define(modelName, attributes, options)设置options.defaultScopeoptions.scopes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lib/sequelize.js
define(modelName, attributes, options) {
options = options || {};

options.modelName = modelName;
options.sequelize = this;

const model = class extends Model {};

// 这里能看到实际上定义的方法是在 model 里面
model.init(attributes, options);

return model;
}

实际上是从 Model.init(attributes, options) 定义并检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// lib/model.js
static init(attributes, options) { // testhint options:none

...

// 初始化 defaultScope 和 scopes
this.options = Object.assign({
timestamps: true,
validate: {},
freezeTableName: false,
underscored: false,
underscoredAll: false,
paranoid: false,
rejectOnEmpty: false,
whereCollection: null,
schema: null,
schemaDelimiter: '',
defaultScope: {},
scopes: [],
indexes: []
}, options);

...

// 赋值 _scope 为 defaultScope
this._scope = this.options.defaultScope;
this._scopeNames = ['defaultScope'];

// 检查 scope 内属性
if (_.isPlainObject(this._scope)) {
this._conformOptions(this._scope, this);
}

_.each(this.options.scopes, scope => {
if (_.isPlainObject(scope)) {
this._conformOptions(scope, this);
}
});

...

return this;
}

第二种

Model.addScope(name, scope, options)增加,如果添加的是重复的或者是defaultScope则需要options.override = true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// lib/model.js
static addScope(name, scope, options) {
options = _.assign({
override: false
}, options);

// 如果添加的是重复 scope 或者是 defaultScope 则需要 options.override = true,否则抛异常
if ((name === 'defaultScope' || name in this.options.scopes) && options.override === false) {
throw new Error('The scope ' + name + ' already exists. Pass { override: true } as options to silence this error');
}

// 同样地,检查 scope 内属性
this._conformOptions(scope, this);

// 如果是 defaultScope 还要赋值 _scope
if (name === 'defaultScope') {
this.options.defaultScope = this._scope = scope;
} else {
this.options.scopes[name] = scope;
}
}

使用

scope通过Model.scope(option)方法调用,该方法可以传入一个或者多个scope名称或者方法,并返回一个全功能的Model,可以调用原来Model的所有方法,如:Model.findAll(options)Model.update(values, options)Model.count(options)Model.destroy(options)等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// lib/model.js
static scope(option) {
const self = class extends this {};
let scope;
let scopeName;

Object.defineProperty(self, 'name', { value: this.name });
// 重置 _scope
self._scope = {};
self._scopeNames = [];
self.scoped = true;
// 如果 option 真值为 false,就去除任何 scope,包括 defaultScope
if (!option) {
return self;
}

// 通过 lodash 展平一级数据嵌套,兼容数组传入或者多参数传入
const options = _.flatten(arguments);

// 开始循环处理每一个 scope
for (const option of options) {
scope = null;
scopeName = null;

if (_.isPlainObject(option)) {
// 处理通过 { method: [ 'scopeName', params] }
if (option.method) {
if (Array.isArray(option.method) && !!self.options.scopes[option.method[0]]) {
scopeName = option.method[0];
// 传入参数调用定义的 scope function
scope = self.options.scopes[scopeName].apply(self, option.method.slice(1));
}
else if (self.options.scopes[option.method]) {
scopeName = option.method;
scope = self.options.scopes[scopeName].apply(self);
}
} else {
// 或者你也可以直接传入一个 scope 实例,如:{attributes, include}
scope = option;
}
} else {
// 处理 string 方式传入
// 如果是 defaultScope,只能是 object 定义
if (option === 'defaultScope' && _.isPlainObject(self.options.defaultScope)) {
scope = self.options.defaultScope;
} else {
scopeName = option;
// 获取对应 object
scope = self.options.scopes[scopeName];
// 如果是 function 即调用引用
if (_.isFunction(scope)) {
scope = scope();
this._conformOptions(scope, self);
}
}
}
// 如果有值,开始处理 scope 合并
if (scope) {
_.assignWith(self._scope, scope, (objectValue, sourceValue, key) => {
// 如果是 where assign 合并,后者替代前者
if (key === 'where') {
return Array.isArray(sourceValue) ? sourceValue : Object.assign(objectValue || {}, sourceValue);
// 如果是 attributes, include, group 其中之一,则 concat 连接
} else if (['attributes', 'include', 'group'].indexOf(key) >= 0 && Array.isArray(objectValue) && Array.isArray(sourceValue)) {
return objectValue.concat(sourceValue);
}
// 其他情况直接 替换
return objectValue ? objectValue : sourceValue;
});

self._scopeNames.push(scopeName ? scopeName : 'defaultScope');
} else {
throw new sequelizeErrors.SequelizeScopeError('Invalid scope ' + scopeName + ' called.');
}
}

return self;
}

值得一提是,Model.scope(option)参数真值为false即去除所有scope,包括defaultScope,也可以通过调用unscoped()

1
2
3
4
// lib/model.js
static unscoped() {
return this.scope();
}

那么其实设置了scopes之后,在查询中如何体现呢?

1
2
3
4
5
6
7
// lib/model.js
static findAll(options) {
...
// 这个方法他会将 Model._scope 合并到 options 里面
this._injectScope(options);
...
}

总结

根据以上源码可以知道,

  • 调用Model.scope(option)之后的Model是一个全功能的Model,只是修改了Model._scope,之前怎么用现在也可以一样用,包括在scope.include里面使用
  • 我们可以通过两种方法定义scope,如果在Sequelize.define(modelName, attributes, options) 不方便定义defaultScope的时候,可以通过Model.addScope(name, scope, { override: true})覆盖
  • 如果定义defaultScope,只能定义Object
  • scope合并规则是key后者覆盖前者
  • attributesincludegroup,不会覆盖,只会concat连接
  • where也不会被覆盖,where里面的key会后者覆盖前者
  • 如果使用多个scope,合并的时候需要注意参数顺序,免得发生预料之外的合并结果
  • 如果不需要scope,调用Model.scope()或者Model.unscope()即可去除