Sequelize Scopes 部分原始碼解析

YeungKC發表於2019-03-04

前言

在使用SequelizeScopes的時候,遇到了一些坑,也很好奇裡面怎樣實現的,所以特意看一遍原始碼~~

定義

定義方式有兩種:

第一種

Sequelize.define(modelName, attributes, options)設定options.defaultScopeoptions.scopes

// 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) 定義並檢查

// 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

// 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)等等

// 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 接引入用
      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()

static unscoped() {
  return this.scope();
}
複製程式碼

那麼其實設定了scopes之後,在查詢中如何體現呢?

// 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})覆蓋
  • wherekey會覆蓋
  • attributesincludegroup,不會覆蓋,只會concat連線
  • 如果使用多個scope,合併的時候需要注意引數順序,免得發生預料之外的合併結果
  • 如果不需要scope,呼叫Model.scope()或者Model.unscope()即可去除

部落格原文地址:Sequelize Scopes 部分原始碼解析

相關文章