若是是常用Node来作服务端开发的童鞋,确定不可避免的会操做数据库,作一些增删改查(CRUD
,Create Read Update Delete
)的操做,若是是一些简单的操做,相似定时脚本什么的,可能就直接生写SQL语句来实现功能了,而若是是在一些大型项目中,数十张、上百张的表,之间还会有一些(一对多,多对多)的映射关系,那么引入一个ORM
(Object Relational Mapping
)工具来帮助咱们与数据库打交道就能够减轻一部分没必要要的工做量,Sequelize
就是其中比较受欢迎的一个。javascript
先来举例说明一下直接拼接SQL
语句这样比较“底层”的操做方式:java
CREATE TABLE animal (
id INT AUTO_INCREMENT,
name VARCHAR(14) NOT NULL,
weight INT NOT NULL,
PRIMARY KEY (`id`)
);
复制代码
建立这样的一张表,三个字段,自增ID、name
以及weight
。
若是使用mysql
这个包来直接操做数据库大概是这样的:node
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
语句都要进行修改(由于各个数据库的方言可能有区别)mysql
关于记忆这件事情,机器确定会比人脑更靠谱儿,因此就有了ORM
,这里就用到了在Node
中比较流行的Sequelize
。git
首先可能须要解释下ORM
是作什么使的,能够简单地理解为,使用面向对象的方式,经过操做对象来实现与数据库以前的交流,完成CRUD
的动做。
开发者并不须要关心数据库的类型,也不须要关心实际的表结构,而是根据当前编程语言中对象的结构与数据库中表、字段进行映射。github
就比如针对上边的animal
表进行操做,再也不须要在代码中去拼接SQL
语句,而是直接调用相似Animal.create
,Animal.find
就能够完成对应的动做。sql
首先咱们要先下载Sequelize
的依赖:typescript
npm i sequelize
npm i mysql2 # 以及对应的咱们须要的数据库驱动
复制代码
而后在程序中建立一个Sequelize
的实例:数据库
const Sequelize = require('Sequelize')
const sequelize = new Sequelize('mysql://root:jarvis@127.0.0.1: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定义模型相关的各类配置:docsnpm
抛开模型定义的部分,使用Sequelize
无疑减轻了不少使用上的成本,由于模型的定义通常不太会去改变,一次定义屡次使用,而使用手动拼接SQL
的方式可能就须要将一段SQL
改来改去的。
并且能够帮助进行字段类型的转换,避免出现类型强制转换出错NaN
或者数字被截断等一些粗心致使的错误。
经过定义模型的方式来告诉程序,有哪些模型,模型的字段都是什么,让程序来帮助咱们记忆,而非让咱们本身去记忆。
咱们只须要拿到对应的模型进行操做就行了。
But,虽然说切换为ORM
工具已经帮助咱们减小了很大一部分的记忆成本,可是依然还不够,咱们仍然须要知道模型中都有哪些字段,才能在业务逻辑中进行使用,若是新人接手项目,仍然须要去翻看模型的定义才能知道有什么字段,因此就有了今天要说的真正的主角儿: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:jarvis@127.0.0.1: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
中一个重要的理念:继承。
由于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
:
// /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
static async getList () {
return this.findAll({raw: true})
}
}
// /app.ts
// 这样就能够直接调用`getList`来实现相似的效果了
await Animal.getList() // 返回一个JSON数组
复制代码
同理,由于上边咱们的两个Dog
和Bird
继承自Animal
,因此代码不用改动就能够直接使用getList
了。
const results = await Dog.getList()
results[0].leg // TS提示错误
复制代码
可是若是你像上边那样使用的话,TS会提示错误的:[ts] 类型“Animal”上不存在属性“leg”。
。
哈哈,这又是为何呢?细心的同窗可能会发现,getList
的返回值是一个Animal[]
类型的,因此上边并无leg
属性,Bird
的两个属性也是如此。
因此咱们须要教TS
认识咱们的数据结构,这样就须要针对Animal
的定义进行修改了,用到了 范型。 咱们经过在函数上边添加一个范型的定义,而且添加限制保证传入的范型类型必定是继承自Animal
的,在返回值转换其类型为T
,就能够实现功能了。
class Animal {
static async getList<T extends Animal>() {
const results = await this.findAll({
raw: true,
})
return results as T[]
}
}
const dogList = await Dog.getList<Dog>()
// 或者不做任何修改,直接在外边手动as也能够实现相似的效果
// 可是这样仍是不太灵活,由于你要预先知道返回值的具体类型结构,将预期类型传递给函数,由函数去组装返回的类型仍是比较推荐的
const dogList = await Dog.getList() as Dog[]
console.log(dogList[0].leg) // success
复制代码
这时再使用leg
属性就不会出错了,若是要使用范型,必定要记住添加extends Animal
的约束,否则TS
会认为这里能够传入任意类型,那么很难保证能够正确的兼容Animal
,可是继承自Animal
的必定是能够兼容的。
固然若是连这里的范型或者as
也不想写的话,还能够在子类中针对父类方法进行重写。
并不须要完整的实现逻辑,只须要获取返回值,而后修改成咱们想要的类型便可:
class Dog extends Animal {
static async getList() {
// 调用父类方法,而后将返回值指定为某个类型
const results = await super.getList()
return results as Dog[]
}
}
// 这样就能够直接使用方法,而不用担忧返回值类型了
const dogList = await Dog.getList()
console.log(dogList[0].leg) // success
复制代码
本文只是一个引子,一些简单的示例,只为体现出三者(SQL
、Sequelize
和Sequelize-typescript
)之间的区别,Sequelize
中有更多高阶的操做,相似映射关系之类的,这些在Sequelize-typescript
中都有对应的体现,并且由于使用了装饰器,实现这些功能所需的代码会减小不少,看起来也会更清晰。
固然了,ORM
这种东西也不是说要一股脑的上,若是是初学者,从我的层面上我不建议使用,由于这样会少了一个接触SQL的机会
若是项目结构也不是很复杂,或者可预期的将来也不会太复杂,那么使用ORM
也没有什么意义,还让项目结构变得复杂起来
以及,必定程度上来讲,通用就意味着妥协,为了保证多个数据库之间的效果都一致,可能会抛弃一些数据库独有的特性,若是明确的须要使用这些特性,那么ORM
也不会太适合
选择最合适的,要知道使用某样东西的意义
最终的一个示例放在了GitHub上:notebook | typescript/sequelize
参考资料: