如果是经常使用Node来做服务端开发的童鞋,肯定不可避免的会操作数据库,做一些增删改查(
CRUD
,
Create
Read
Update
Delete
)的操作,如果是一些简单的操作,类似定时脚本什么的,可能就直接生写SQL语句来实现功能了,而如果是在一些大型项目中,数十张、上百张的表,之间还会有一些(一对多,多对多)的映射关系,那么引入一个
ORM
(
Object
Relational
Mapping
)工具来帮助我们与数据库打交道就可以减轻一部分不必要的工作量,
Sequelize
就是其中比较受欢迎的一个。
CRUD原始版:手动拼接SQL
先来举例说明一下直接拼接
SQL
语句这样比较“底层”的操作方式:
CREATE TABLE animal (
id INT AUTO_INCREMENT,
name VARCHAR(14) NOT NULL,
weight INT NOT NULL,
PRIMARY KEY (`id`)
);
创建这样的一张表,三个字段,自增ID、
name
以及
weight
。
如果使用
mysql
这个包来直接操作数据库大概是这样的:
const connection = mysql
.createConnection({})
const tableName = 'animal'
connection.connect()
// 我们假设已经支持了Promise
// 查询
const [results] = await connection.query(`
SELECT
id,
name,
weight
FROM ${tableName}
`)
// 新增
const name = 'Niko'
const weight = 70
await connection
.query(`
INSERT INTO ${tableName} (name, weight)
VALUES ('${name}', ${weight})
`)
// 或者通过传入一个Object的方式也可以做到
await connection.query(`INSERT INTO ${tableName} SET ?`, {
name,
weight
})
connection.end()
看起来也还算是比较清晰,但是这样带来的问题就是,开发人员需要对表结构足够的了解。
如果表中有十几个字段,对于开发人员来说这会是很大的记忆成本,你需要知道某个字段是什么类型,拼接
SQL
时还要注意插入时的顺序及类型,
WHERE
条件对应的查询参数类型,如果修改某个字段的类型,还要去处理对应的传参。
这样的项目尤其是在进行交接的时候更是一件恐怖的事情,新人又需要从头学习这些表结构。
以及还有一个问题,如果有哪天需要更换数据库了,放弃了
MySQL
,那么所有的
SQL
语句都要进行修改(因为各个数据库的方言可能有区别)。
CRUD进阶版 Sequelize的使用
关于记忆这件事情,机器肯定会比人脑更靠谱儿,所以就有了
ORM
,这里就用到了在
Node
中比较流行的
Sequelize
。
ORM是干嘛的
首先可能需要解释下
ORM
是做什么使的,可以简单地理解为,使用面向对象的方式,通过操作对象来实现与数据库之前的交流,完成
CRUD
的动作。
开发者并不需要关心数据库的类型,也不需要关心实际的表结构,而是根据当前编程语言中对象的结构与数据库中表、字段进行映射。
就好比针对上边的
animal
表进行操作,不再需要在代码中去拼接
SQL
语句,而是直接调用类似
Animal
.
create
,
Animal
.
find
就可以完成对应的动作。
Sequelize的使用方式
首先我们要先下载
Sequelize
的依赖:
npm i sequelize
npm i mysql2 # 以及对应的我们需要的数据库驱动
然后在程序中创建一个
Sequelize
的实例:
const Sequelize = require('Sequelize')
const sequelize = new Sequelize('mysql://root:[email protected]:3306/ts_test')
// dialect://username:password@host:port/db_name
// 针对上述的表,我们需要先建立对应的模型:
const Animal
= sequelize.define('animal', {
id: { type: Sequelize.INTEGER, autoIncrement: true },
name: { type: Sequelize.STRING, allowNull: false },
weight: { type: Sequelize.INTEGER, allowNull: false },
}, {
// 禁止sequelize修改表名,默认会在animal后边添加一个字母`s`表示负数
freezeTableName: true,
// 禁止自动添加时间戳相关属性
timestamps: false,
})
// 然后就可以开始使用咯
// 还是假设方法都已经支持了Promise
// 查询
const results = await Animal.findAll({
raw: true,
})
// 新增
const name = 'Niko'
const weight = 70
await Animal.create({
name,
weight,
})
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
,所以环境依赖上要安装的东西会多一些:
# 这里采用ts-node来完成举例
npm i ts-node typescript
npm i sequelize reflect-metadata sequelize-typescript
其次,还需要修改
TS
项目对应的
tsconfig
.
json
文件,用来让
TS
支持装饰器的使用:
{
"compilerOptions": {
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
}
}
然后就可以开始编写脚本来进行开发了,与
Sequelize
不同之处基本在于模型定义的地方:
// /modles/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'
@Table({
tableName: 'animal'
})
export class Animal extends Model<Animal> {
@Column({
primaryKey: true,
autoIncrement: true,
})
id: number
@Column
name: string
@Column
weight: number
}
// 创建与数据库的链接、初始化模型
// app.ts
import path from 'path'
import { Sequelize } from 'sequelize-typescript'
import Animal from './models/animal'
const sequelize = new Sequelize('mysql://root:[email protected]:3306/ts_test')
sequelize.addModels([path.resolve(__dirname, `./models/`)])
// 查询
const results = await Animal.findAll({
raw: true,
})
// 新增
const name = 'Niko'
const weight = 70
await Animal.create({
name,
weight,
})
与普通的
Sequelize
不同的有这么几点:
-
模型的定义采用装饰器的方式来定义
-
实例化
Sequelize
对象时需要指定对应的
model
路径
-
模型相关的一系列方法都是支持
Promise
的
如果在使用过程中遇到提示
XXX used before model init
,可以尝试在实例化前边添加一个
await
操作符,等到与数据库的连接建立完成以后再进行操作。
但是好像看起来这样写的代码相较于
Sequelize
多了不少呢,而且至少需要两个文件来配合,那么这么做的意义是什么的?
答案就是
OOP
中一个重要的理念:继承。
使用Sequelize-typescript实现模型的继承
因为
TypeScript
的核心开发人员中包括
C
#
的架构师,所以
TypeScript
中可以看到很多类似
C
#
的痕迹,在模型的这方面,我们可以尝试利用继承减少一些冗余的代码。
比如说我们基于
animal
表又有了两张新表,
dog
和
bird
,这两者之间肯定是有区别的,所以就有了这样的定义:
CREATE TABLE dog (
id INT AUTO_INCREMENT,
name VARCHAR(14) NOT NULL,
weight INT NOT NULL,
leg INT NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE bird (
id INT AUTO_INCREMENT,
name VARCHAR(14)
NOT NULL,
weight INT NOT NULL,
wing INT NOT NULL,
claw INT NOT NULL,
PRIMARY KEY (`id`)
);
关于
dog
我们有一个腿
leg
数量的描述,关于
bird
我们有了翅膀
wing
和爪子
claw
数量的描述。
特意让两者的特殊字段数量不同,省的有杠精说可以通过添加
type
字段区分两种不同的动物 :p
如果要用
Sequelize
的方式,我们就要将一些相同的字段定义
define
三遍才能实现,或者说写得灵活一些,将
define
时使用的
Object
抽出来使用
Object
.
assign
的方式来实现类似继承的效果。
但是在
Sequelize
-
typescript
就可以直接使用继承来实现我们想要的效果:
// 首先还是我们的Animal模型定义
// /models/animal.ts
import { Table, Column
, Model } from 'sequelize-typescript'
@Table({
tableName: 'animal'
})
export default class Animal extends Model<Animal> {
@Column({
primaryKey: true,
autoIncrement: true,
})
id: number
@Column
name: string
@Column
weight: number
}
// 接下来就是继承的使用了
// /models/dog.ts
import { Table, Column, Model } from 'sequelize-typescript'
import Animal from './animal'
@Table({
tableName: 'dog'
})
export default class Dog extends Animal {
@Column
leg: number
}
// /models/bird.ts
import { Table,
Column, Model } from 'sequelize-typescript'
import Animal from './animal'
@Table({
tableName: 'bird'
})
export default class Bird extends Animal {
@Column
wing: number
@Column
claw: number
}
有一点需要注意的:
每一个模型需要单独占用一个文件,并且采用
export
default
的方式来导出
。
也就是说目前我们的文件结构是这样的:
├── models
│ ├── animal.ts
│ ├── bird.ts
│ └── dog.ts
└── app.ts
得益于
TypeScript
的静态类型,我们能够很方便地得知这些模型之间的关系,以及都存在哪些字段。
在结合着
VS
Code
开发时可以得到很多动态提示,类似
findAll
,
create
之类的操作都会有提示:
Animal.create<Animal>({
abc: 1,
// ^ abc不是Animal已知的属性
})
通过继承来复用一些行为
上述的例子也只是说明了如何复用模型,但是如果是一些封装好的方法呢?
类似的获取表中所有的数据,可能一般情况下获取
JSON
数据就够了,也就是
findAll
({
raw
:
true
})
。
所以我们可以针对类似这样的操作进行一次简单的封装,不需要开发者手动去调用
findAll