专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  Bun ... ·  21 小时前  
码农翻身  ·  漫画 | 为什么大家都愿意进入外企? ·  昨天  
程序员的那些事  ·  清华大学:DeepSeek + ... ·  2 天前  
程序员的那些事  ·  OpenAI ... ·  昨天  
程序员小灰  ·  清华大学《DeepSeek学习手册》(全5册) ·  2 天前  
51好读  ›  专栏  ›  SegmentFault思否

使用 TS + Sequelize 实现更简洁的 CRUD

SegmentFault思否  · 公众号  · 程序员  · 2018-09-17 08:00

正文

如果是经常使用Node来做服务端开发的童鞋,肯定不可避免的会操作数据库,做一些增删改查( CRUD Create Read Update Delete )的操作,如果是一些简单的操作,类似定时脚本什么的,可能就直接生写SQL语句来实现功能了,而如果是在一些大型项目中,数十张、上百张的表,之间还会有一些(一对多,多对多)的映射关系,那么引入一个 ORM Object Relational Mapping )工具来帮助我们与数据库打交道就可以减轻一部分不必要的工作量, Sequelize 就是其中比较受欢迎的一个。

CRUD原始版:手动拼接SQL

先来举例说明一下直接拼接 SQL 语句这样比较“底层”的操作方式:

  1. CREATE TABLE animal (

  2.  id INT AUTO_INCREMENT,

  3.  name VARCHAR(14) NOT NULL,

  4.  weight INT NOT NULL,

  5.  PRIMARY KEY (`id`)

  6. );

创建这样的一张表,三个字段,自增ID、 name 以及 weight

如果使用 mysql 这个包来直接操作数据库大概是这样的:

  1. const connection = mysql .createConnection({})

  2. const tableName = 'animal'

  3. connection.connect()

  4. // 我们假设已经支持了Promise

  5. // 查询

  6. const [results] = await connection.query(`

  7.  SELECT

  8.    id,

  9.    name,

  10.    weight

  11.  FROM ${tableName}

  12. `)

  13. // 新增

  14. const name = 'Niko'

  15. const weight = 70

  16. await connection .query(`

  17.  INSERT INTO ${tableName} (name, weight)

  18.  VALUES ('${name}', ${weight})

  19. `)

  20. // 或者通过传入一个Object的方式也可以做到

  21. await connection.query(`INSERT INTO ${tableName} SET ?`, {

  22.  name,

  23.  weight

  24. })

  25. connection.end()

看起来也还算是比较清晰,但是这样带来的问题就是,开发人员需要对表结构足够的了解。

如果表中有十几个字段,对于开发人员来说这会是很大的记忆成本,你需要知道某个字段是什么类型,拼接 SQL 时还要注意插入时的顺序及类型, WHERE 条件对应的查询参数类型,如果修改某个字段的类型,还要去处理对应的传参。

这样的项目尤其是在进行交接的时候更是一件恐怖的事情,新人又需要从头学习这些表结构。

以及还有一个问题,如果有哪天需要更换数据库了,放弃了 MySQL ,那么所有的 SQL 语句都要进行修改(因为各个数据库的方言可能有区别)。

CRUD进阶版 Sequelize的使用

关于记忆这件事情,机器肯定会比人脑更靠谱儿,所以就有了 ORM ,这里就用到了在 Node 中比较流行的 Sequelize

ORM是干嘛的

首先可能需要解释下 ORM 是做什么使的,可以简单地理解为,使用面向对象的方式,通过操作对象来实现与数据库之前的交流,完成 CRUD 的动作。

开发者并不需要关心数据库的类型,也不需要关心实际的表结构,而是根据当前编程语言中对象的结构与数据库中表、字段进行映射。

就好比针对上边的 animal 表进行操作,不再需要在代码中去拼接 SQL 语句,而是直接调用类似 Animal . create Animal . find 就可以完成对应的动作。

Sequelize的使用方式

首先我们要先下载 Sequelize 的依赖:

  1. npm i sequelize

  2. npm i mysql2    # 以及对应的我们需要的数据库驱动

然后在程序中创建一个 Sequelize 的实例:

  1. const Sequelize = require('Sequelize')

  2. const sequelize = new Sequelize('mysql://root:[email protected]:3306/ts_test')

  3. //                             dialect://username:password@host:port/db_name

  4. // 针对上述的表,我们需要先建立对应的模型:

  5. const Animal = sequelize.define('animal', {

  6.  id: { type: Sequelize.INTEGER, autoIncrement: true },

  7.  name: { type: Sequelize.STRING, allowNull: false },

  8.  weight: { type: Sequelize.INTEGER, allowNull: false },

  9. }, {

  10.  // 禁止sequelize修改表名,默认会在animal后边添加一个字母`s`表示负数

  11.  freezeTableName: true,

  12.  // 禁止自动添加时间戳相关属性

  13.  timestamps: false,

  14. })

  15. // 然后就可以开始使用咯

  16. // 还是假设方法都已经支持了Promise

  17. // 查询

  18. const results = await Animal.findAll({

  19.  raw: true,

  20. })

  21. // 新增

  22. const name = 'Niko'

  23. const weight = 70

  24. await Animal.create({

  25.  name,

  26.  weight,

  27. })

sequelize定义模型相关的各种配置:docs( https://github.com/demopark/sequelize-docs-Zh-CN/blob/master/models-definition.md

抛开模型定义的部分,使用 Sequelize 无疑减轻了很多使用上的成本,因为模型的定义一般不太会去改变,一次定义多次使用,而使用手动拼接 SQL 的方式可能就需要将一段 SQL 改来改去的。

而且可以帮助进行字段类型的转换,避免出现类型强制转换出错 NaN 或者数字被截断等一些粗心导致的错误。

通过定义模型的方式来告诉程序,有哪些模型,模型的字段都是什么,让程序来帮助我们记忆,而非让我们自己去记忆。

我们只需要拿到对应的模型进行操作就好了。

这还不够

But ,虽说切换为 ORM 工具已经帮助我们减少了很大一部分的记忆成本,但是依然还不够,我们仍然需要知道模型中都有哪些字段,才能在业务逻辑中进行使用,如果新人接手项目,仍然需要去翻看模型的定义才能知道有什么字段,所以就有了今天要说的真正的主角儿:sequelize-typescript

CRUD终极版 装饰器实现模型定义

Sequelize - typescript 是基于 Sequelize 针对 TypeScript 所实现的一个增强版本,抛弃了之前繁琐的模型定义,使用装饰器直接达到我们想到的目的。

Sequelize-typescript的使用方式

首先因为是用到了 TS ,所以环境依赖上要安装的东西会多一些:

  1. # 这里采用ts-node来完成举例

  2. npm i ts-node typescript

  3. npm i sequelize reflect-metadata sequelize-typescript

其次,还需要修改 TS 项目对应的 tsconfig . json 文件,用来让 TS 支持装饰器的使用:

  1. {

  2.  "compilerOptions": {

  3. +   "experimentalDecorators": true,

  4. +   "emitDecoratorMetadata": true

  5.  }

  6. }

然后就可以开始编写脚本来进行开发了,与 Sequelize 不同之处基本在于模型定义的地方:

  1. // /modles/animal.ts

  2. import { Table, Column, Model } from 'sequelize-typescript'

  3. @Table({

  4.  tableName: 'animal'

  5. })

  6. export class Animal extends Model<Animal> {

  7.  @Column({

  8.    primaryKey: true,

  9.    autoIncrement: true,

  10.  })

  11.  id: number

  12.  @Column

  13.  name: string

  14.  @Column

  15.  weight: number

  16. }

  17. // 创建与数据库的链接、初始化模型

  18. // app.ts

  19. import path from 'path'

  20. import { Sequelize } from 'sequelize-typescript'

  21. import Animal from './models/animal'

  22. const sequelize = new Sequelize('mysql://root:[email protected]:3306/ts_test')

  23. sequelize.addModels([path.resolve(__dirname, `./models/`)])

  24. // 查询

  25. const results = await Animal.findAll({

  26.  raw: true,

  27. })

  28. // 新增

  29. const name = 'Niko'

  30. const weight = 70

  31. await Animal.create({

  32.  name,

  33.  weight,

  34. })

与普通的 Sequelize 不同的有这么几点:

  1. 模型的定义采用装饰器的方式来定义

  2. 实例化 Sequelize 对象时需要指定对应的 model 路径

  3. 模型相关的一系列方法都是支持 Promise

如果在使用过程中遇到提示 XXX used before model init ,可以尝试在实例化前边添加一个 await 操作符,等到与数据库的连接建立完成以后再进行操作。

但是好像看起来这样写的代码相较于 Sequelize 多了不少呢,而且至少需要两个文件来配合,那么这么做的意义是什么的?

答案就是 OOP 中一个重要的理念:继承。

使用Sequelize-typescript实现模型的继承

因为 TypeScript 的核心开发人员中包括 C # 的架构师,所以 TypeScript 中可以看到很多类似 C # 的痕迹,在模型的这方面,我们可以尝试利用继承减少一些冗余的代码。

比如说我们基于 animal 表又有了两张新表, dog bird ,这两者之间肯定是有区别的,所以就有了这样的定义:

  1. CREATE TABLE dog (

  2.  id INT AUTO_INCREMENT,

  3.  name VARCHAR(14) NOT NULL,

  4.  weight INT NOT NULL,

  5.  leg INT NOT NULL,

  6.  PRIMARY KEY (`id`)

  7. );

  8. CREATE TABLE bird (

  9.  id INT AUTO_INCREMENT,

  10.  name VARCHAR(14) NOT NULL,

  11.  weight INT NOT NULL,

  12.  wing INT NOT NULL,

  13.  claw INT NOT NULL,

  14.  PRIMARY KEY (`id`)

  15. );

关于 dog 我们有一个腿 leg 数量的描述,关于 bird 我们有了翅膀 wing 和爪子 claw 数量的描述。

特意让两者的特殊字段数量不同,省的有杠精说可以通过添加 type 字段区分两种不同的动物 :p

如果要用 Sequelize 的方式,我们就要将一些相同的字段定义 define 三遍才能实现,或者说写得灵活一些,将 define 时使用的 Object 抽出来使用 Object . assign 的方式来实现类似继承的效果。

但是在 Sequelize - typescript 就可以直接使用继承来实现我们想要的效果:

  1. // 首先还是我们的Animal模型定义

  2. // /models/animal.ts

  3. import { Table, Column , Model } from 'sequelize-typescript'

  4. @Table({

  5.  tableName: 'animal'

  6. })

  7. export default class Animal extends Model<Animal> {

  8.  @Column({

  9.    primaryKey: true,

  10.    autoIncrement: true,

  11.  })

  12.  id: number

  13.  @Column

  14.  name: string

  15.  @Column

  16.  weight: number

  17. }

  18. // 接下来就是继承的使用了

  19. // /models/dog.ts

  20. import { Table, Column, Model } from 'sequelize-typescript'

  21. import Animal from './animal'

  22. @Table({

  23.  tableName: 'dog'

  24. })

  25. export default class Dog extends Animal {

  26.  @Column

  27.  leg: number

  28. }

  29. // /models/bird.ts

  30. import { Table, Column, Model } from 'sequelize-typescript'

  31. import Animal from './animal'

  32. @Table({

  33.  tableName: 'bird'

  34. })

  35. export default class Bird extends Animal {

  36.  @Column

  37.  wing: number

  38.  @Column

  39.  claw: number

  40. }

有一点需要注意的: 每一个模型需要单独占用一个文件,并且采用 export default 的方式来导出

也就是说目前我们的文件结构是这样的:

  1. ├── models

  2.    ├── animal.ts

  3.    ├── bird.ts

  4.    └── dog.ts

  5. └── app.ts

得益于 TypeScript 的静态类型,我们能够很方便地得知这些模型之间的关系,以及都存在哪些字段。

在结合着 VS Code 开发时可以得到很多动态提示,类似 findAll create 之类的操作都会有提示:

  1. Animal.create<Animal>({

  2.  abc: 1,

  3. // ^ abc不是Animal已知的属性  

  4. })

通过继承来复用一些行为

上述的例子也只是说明了如何复用模型,但是如果是一些封装好的方法呢?

类似的获取表中所有的数据,可能一般情况下获取 JSON 数据就够了,也就是 findAll ({ raw : true })

所以我们可以针对类似这样的操作进行一次简单的封装,不需要开发者手动去调用 findAll







请到「今天看啥」查看全文