使用TS+Sequelize实现更简洁的CRUD
如果是经常使用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: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定义模型相关的各种配置:docs
抛开模型定义的部分,使用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: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中一个重要的理念:继承。
使用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:
// /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
参考资料:
使用TS+Sequelize实现更简洁的CRUD的更多相关文章
- ssm+redis 如何更简洁的利用自定义注解+AOP实现redis缓存
基于 ssm + maven + redis 使用自定义注解 利用aop基于AspectJ方式 实现redis缓存 如何能更简洁的利用aop实现redis缓存,话不多说,上demo 需求: 数据查询时 ...
- jquery-ui 的 主题 选择什么颜色? 建议使用html5 的标准进行书写, 更简洁!
jQuery ui有多种主体, 基本上, 不能使用 no theme 的"主题包" base: 是基本的, 颜色以深灰色为主, 高亮显示为蓝色, ui lightness(明快) ...
- 更简洁的 CSS 清理浮动方式
CSS清理浮动有很多种方式,像使用 br 标签自带的 clear 属,使用元素的 overflow,使用空标签来设置 clear:both 等等.但考虑到兼容问题和语义化的问题,一般我们都会使用如下代 ...
- wordpress去掉category怎么操作让url更简洁友好
用wordpress建站是比较流行的,全球将近25%的站点是用wordpress搭建的.有很多的模板.插件可以选择,当然最好还是能自己优化.URL固定链接就是之中一个基础的技巧.有网友问如何去掉url ...
- 使用Groovy+Spock轻松写出更简洁的单测
当无法避免做一件事时,那就让它变得更简单. 概述 单测是规范的软件开发流程中的必不可少的环节之一.再伟大的程序员也难以避免自己不犯错,不写出有BUG的程序.单测就是用来检测BUG的.Java阵营中,J ...
- leetcode 题解 word search。递归可以更简洁。
先写上我的代码: 我总是不知道何时把任务交给下一个递归.以致于,写出的代码很臃肿! 放上别人递归的简洁代码: bool exist(char** board, int m, int n, char* ...
- php 中更简洁的三元运算符 ?:
PHP 三元运算符是对参数赋值时候的一个简洁的主要用法. 一个主要的用法: PHP 三元运算符能够让你在一行代码中描述判定代码, 从而替换掉类似以下的代码: <?php if (isset($v ...
- OGNL(Object-Graph Navigation Language),可以方便地操作对象属性的开源表达式语言,使页面更简洁;
OGNL(Object-Graph Navigation Language),可以方便地操作对象属性的开源表达式语言,使页面更简洁: 支持运算符(如+-*/),比普通的标志具有更高的自由度和更强的功能 ...
- C++11/14的新特性——更简洁
新的字符串表示方式——原生字符串(Raw String Literals) C/C++中提供了字符串,字符串的转义序列,给输出带来了很多不变,如果需要原生义的时候,需要反转义,比较麻烦. C++提 ...
随机推荐
- C++中sizeof操作符与strlen函数
sizeof操作符: sizeof是一个操作符,返回一条表达式或一个类型名字所占的字节数.返回值一个常量表达式,类型为size_t. size_t sizeof(type) size_t sizeof ...
- Memcache CAS协议介绍及使用
1.什么是CAS 所谓CAS,check and set,在写操作时,先检查是否被别的线程修改过. 基本原理非常简单,一言以蔽之,就是"版本号".每个存储的数据对象,多有一个版本号 ...
- DAY2-Flask项目
回顾: 1.安装pipenv虚拟运行环境,隔离项目 (启动:pipenv shell) 2.安装flask(pipenv install shell),查看项目依赖(pipenv graph) 3.查 ...
- 【About Me】 — 有关于我的 —
HNSDFZ信息组一直非常蒻的一只蒟蒻,正在朝着大佬与正解的方向不懈努力中. 目前还是一只高一的萌新,下个学期进高二就可以升级当学姐啦……٩(๑>◡<๑)۶ 呜呜呜已经高二啦!现在高二了 ...
- 【Cf Edu #47 F】Dominant Indices(长链剖分)
要求每个点子树中节点最多的层数,一个通常的思路是树上启发式合并,对于每一个点,保留它的重儿子的贡献,暴力扫轻儿子将他们的贡献合并到重儿子里来. 参考重链剖分,由于一个点向上最多只有$log$条轻边,故 ...
- Fork/Join框架实现原理
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,而ForkJoi ...
- 最新版的Android4.4.2 SDK无法下载解决
http://hi.baidu.com/petercao2008/item/65362d2bdbddfacba5275a50 问题: Downloading ARM EABI v7a System I ...
- 解题:BZOJ 2673 World Final 2011 Chips Challenge
题面 数据范围看起来很像网络流诶(滚那 因为限制多而且强,数据范围也不大,我们考虑不直接求答案,而是转化为判定问题 可以发现第二个限制相对好满足,我们直接枚举这个限制就可以.具体来说是枚举所有行中的最 ...
- [NOI2011]阿狸的打字机——AC自动机之fail树的利用
Description 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机.打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P'两个字母. 经阿狸研究发现,这个打字机是这样工作的 ...
- 特征选择实践---python
作者:城东链接:https://www.zhihu.com/question/28641663/answer/110165221来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...