1629 字
8 分钟
sequelize V5 升级记录以及编译时与运行时问题解决方案

最近把项目中的 sequelize 由 4.38.0 升级到了 5.8.7,以下是升级记录

本文地址: https://blog.zhequtao.com/post/sequelize-upgrade/

01 删包#

从 package.json 中删掉 sequelize 以及 @types/sequelize

02 过文档#

大致过一遍官方升级文档 http://docs.sequelizejs.com/manual/upgrade-to-v5.html

03 npm install#

由于官方提供了 typescript 的支持,不需要在安装 @types/sequelize

Terminal window
npm install sequelize

04 tsc#

由于使用了 typescript 编译,解决问题。

Terminal window
$ tsc
...
Found 1361 errors.

05 new Sequelize#

从数据库初始化入手,解决一些 Sequelize 实例化时的类型问题

06 AnyModel & AnyPropModel & Sequelize.define#

由于 sequelizetype 此时由官方维护,重新定义了 Model 等类型。

虽然目前官方已经支持了对 Model 的 typescript 支持,但是为了更小幅度的升级,仍然使用 Sequelize.define

以后将 AnyPropModel 逐渐替换为 UserModel 等真实的 Model。

根据文档,对 Model 以及 Sequelize.define 做以下更改。

class AnyPropModel extends Model {
[key: string]: any;
}
export type AnyModel = typeof Model & {
new (values?: any, options?: BuildOptions): AnyPropModel;
}
export type Models = Record<string, AnyModel>;
const UserModel = <AnyModel>sequelize.define('MyDefineModel', {
id: {
primaryKey: true,
type: DataTypes.INTEGER.UNSIGNED,
}
});

更改之后再次编译.

Terminal window
$ tsc
Found 898 errors.

07 归并与分类,逐个击破#

对 typescript 编译出来的错误信息进行格式化并做统计,以下为格式化数据,抽出 { file, code, message }

本来也想抽出 lines,没有成功…

Terminal window
$ tsc | grep 'error TS' | jq -R -c -s 'split("\n") | map(capture("(?<file>.+): error (?<code>.+): (?<message>.+)")) | .[]' > build.jsonl
$ head -3 build.jsonl
{"file":"bin/demo.ts(278,23)","code":"TS2351","message":"Cannot use 'new' with an expression whose type lacks a call or construct signature."}
{"file":"src/helpers/one.ts(36,23)","code":"TS2576","message":"Property 'literal' is a static member of type 'Sequelize'"}
{"file":"src/helpers/two.ts(188,24)","code":"TS2576","message":"Property 'fn' is a static member of type 'Sequelize'"}

根据格式化信息,对相同 code 的错误进行分类,先解决错误率最高的五类

Terminal window
$ cat build.jsonl | jq -s 'group_by(.code) | map({count: length, code: .[0].code, message: .[0].message}) | sort_by(.count) | .[]'
{
"count": 41,
"code": "TS6133",
"message": "'DataTypes' is declared but its value is never read."
}
{
"count": 53,
"code": "TS2339",
"message": "Property 'id' does not exist on type 'object'."
}
{
"count": 82,
"code": "TS2709",
"message": "Cannot use namespace 'DataTypes' as a type."
}
{
"count": 94,
"code": "TS7006",
"message": "Parameter 'e' implicitly has an 'any' type."
}
{
"count": 142,
"code": "TS2576",
"message": "Property 'col' is a static member of type 'Sequelize'"
}
{
"count": 335,
"code": "TS2531",
"message": "Object is possibly 'null'."
}

08 TS2576: Sequelize.prototype -> Sequelize#

Terminal window
"Property 'col' is a static member of type 'Sequelize'"
"Property 'literal' is a static member of type 'Sequelize'"
"Property 'or' is a static member of type 'Sequelize'"

根据文档 http://docs.sequelizejs.com/manual/upgrade-to-v5.html#sequelize

Sequelize 示例上的很多方法变成了 static method.

借助 VS Code 与其内置的命令行工具,输入命令 tsc | grep 2576 可以更快解决问题:

  1. tsc | grep 2576 提供所有的此类问题与行号
  2. VS Code 可以根据行号快速定位

但不管怎么说,这还是一个体力活…

09 TS2339 Model 的废弃方法#

Terminal window
Property 'findById' does not exist on type 'AnyModel'.
Property 'find' does not exist on type 'AnyModel'.
Property 'id' does not exist on type 'AnyPropModel | null'.
Property 'LOCK' does not exist on type 'Transaction'.

这个在升级文档中也提到过 http://docs.sequelizejs.com/manual/upgrade-to-v5.html#model

至于替换也是一个体力活,再次编译

Found 753 errors.

10 TS2531 Object is possibly ‘null’#

Terminal window
$ tsc | grep 2531 | wc
406

再次统计下此问题的个数,比刚才统计时多了一百多

我们总是在不停地解决 Bug 的过程中引入新的 Bug。在解决旧 Bug 的过程中总有产生新 Bug 的风险

findById 更改之后有更多的问题显现出来,是因为没有对返回的数据做不存在断言处理,如下例所示

const user = await models.user.findOne()
// 此时 user 可能不存在,可能报错
const id = user.id

在解决问题之前,我先分析下原因

使用 rejectOnEmpty 来修正它,他能保证数据一定存在

const user = await models.User.findOne({
rejectOnEmpty: true
})

接下来就是体力活了:

  1. tsc | grep 2531 提供所有的此类问题与行号
  2. VS Code 根据行号快速定位
  3. gd vim 的 Go to Def 可以快速定位到出问题的变量定义处
  4. "0p 使用 vim 把 rejectOnEmpty 至于0号寄存器,快速粘贴
  5. == vim 进行格式化

再次编译:

Found 335 errors.

11 migration#

Terminal window
TS2709: Cannot use namespace 'DataTypes' as a type

这都是在 migration 文件中的内容,既然数据库迁移脚本已经执行过了,它其实也没多大用处了,我觉得不改也可以了…

另外,migration 这种数据库迁移脚本是不是可以单独从项目中抽出来,有两个原因

  1. 它是一次性脚本
  2. 一个数据库有可能对应多个后端应用
import { QueryInterface, DataTypes } from 'sequelize'
export const up = async function (queryInterface: QueryInterface, Sequelize: DataTypes) {
}

全局替换

Sequelize: DataTypes -> sequelize: typeof Sequelize

Found 216 errors.

12 implicitly any#

Terminal window
TS7006 Parameter 'item' implicitly has an 'any' type.

解决后再次编译

Found 185 errors.

13 使用 sed 批量替换 Op#

// 替换前
where.count = { $lte: 10 }
where.count['$lte'] = 10
where.count.$lte = 10
// 替换后
where.count = { [Op.lte]: 10 }
where.count[Op.lte] = 10
where.count[Op.lte] = 10

写一段 sed 脚本来批量替换

再次编译

Found 412 errors.

14 补充 Op#

以上错误的原因过多是因为批量替换成 Op 后提示 Op 不存在

import { Op } from 'sequelize'

15 再次统计#

Terminal window
$ tsc | grep 'error TS' | jq -R -s 'split("\n") | map(capture("(?<file>.+): error (?<code>.+): (?<message>.+)")) | group_by(.code) | map({count: length, code: .[0].code, message: .[0].message}) | sort_by(.count) | .[]'
{
"count": 12,
"code": "TS2684",
"message": "The 'this' context of type 'typeof Model' is not assignable to method's 'this' of type '(new () => Model<{}, {}>) & typeof Model'."
}
{
"count": 14,
"code": "TS2322",
"message": "Type 'true' is not assignable to type 'false'."
}
{
"count": 18,
"code": "TS2694",
"message": "Namespace '\"/Users/shanyue/backend/node_modules/sequelize/types/index\"' has no exported member 'AnyFindOptions'."
}
{
"count": 20,
"code": "TS2532",
"message": "Object is possibly 'undefined'."
}
{
"count": 118,
"code": "TS2304",
"message": "Cannot find name 'Op'."
}

都是一些小问题了,逐个解决

16 git diff#

统计下修改了多少内容

Terminal window
$ git diff master --shortstat
199 files changed, 1784 insertions(+), 1411 deletions(-)

17 运行时问题: postgres range#

以上编译时的问题解决了,最令人头疼的还是运行时问题了

这次碰到的是 postgres 的 range 这个数据类型

// 更改前
[0, 100]
// 更改后
[{
value: 0,
inclusive: false
}, {
value: 100,
inclusive: true
}]

但是如果对数据库的每个 Model 都加上 type 的话,这个问题就可以在编译时解决

18 运行时问题: undefined in where#

在 where 中遇到 undefined 会抛出异常。https://github.com/sequelize/sequelize/pull/9548/files

使用 _.pickBy 过滤掉 undefined

const where = _.pickBy(data, x => x !== undefined)

当然,如果 typescript 做的比较严格的话,这个问题也可以避免

19 运行时问题: _.assign({ [Op.ne]: 3 })#

_.assign 会丢失 Symbol 属性,使用 Object.assign 代替


欢迎关注我的公众号中关村程序员,在这里记录着我的技术成长,欢迎交流

欢迎关注公众号中关村程序员,在这里记录我的技术成长,欢迎交流

sequelize V5 升级记录以及编译时与运行时问题解决方案
https://blog.zhequtao.com/posts/post/sequelize-upgrade/
作者
阿俊
发布于
2020-03-30
许可协议
CC BY-NC-SA 4.0

相关阅读