业界mysql的实践方案调研

分类

现存于 Node.js 和 TypeScript 生态环境中的数据库工具可以分为三类。

手写 SQL

自己手写 SQL(比如使用 pg 和 mysql 这样的 Node.js 数据库驱动)当然可以完全控制数据库操作,但是,生产力却不高,而且会遇到很多细碎的事情(如手动处理链接、操作模板)。

这种方式的另一个问题在于你获取的查询结果时不是类型安全的。你可能会手动书写这些查询结果的类型,但这会花费大量的时间;另外,如果你对数据库进行了改动,你的类型文件也需要保持一致才行,这也会花费大量时间。

此外,手动构造 SQL 字符串的时候,编辑器没法给你任何提示(只能提示一些 SQL 关键字),效率极差。

特点:控制力极强,生产力弱

SQL 构造器

有不少人用的解决方案是使用 SQL 构造器(如 knex.jsSQL Bricks)以提高生产力。这种工具为构建 SQL 语句提供了封装层次较高的 API。

但最大的问题在于这种工具需要开发者从 SQL 的角度来对待数据,但应用数据往往是关系型的对象,这就会导致了数据在认知层面与实际层面的差异。开发者不得不经常切换思维模型才能写好 SQL 语句。

特点:控制力强,生产力一般

ORM

ORM 可以让开发者将所有数据定义为 class,一个 class 就是一个数据表,开发者不需要对 SQL 有那么深的理解了。

你可以通过 class 的方法来对数据库进行读写,非常方便,而且也非常接近开发者的心智模型。

特点:控制力弱,生产力不错

调研情况

针对上面三种类型,由于手写SQL这里则不做过多阐述,为了避免SQL注入和代码可读性,而且写原生SQL对于大部分前端同学来说真的是一种折磨。

所以对业界常见的SQL构造器(QueryBuilder)和ORM框架做一个调研。

ORM

NodeJS社区中主流的ORM主要有这么几个,它们都有各自的一些特色:

Sequelize

一款老牌的ORM框架,缺点是TS支持不太好,不过社区有给出对应的解决方案Sequelize-Typescript,当然后续官方v6版本后也支持了TS,具体可以参考:Link

具体用法参考:https://docs.nestjs.com/techniques/database#sequelize-integration

TypeORM

NodeJS社区star最多的一个ORM,亮点在基于装饰器语法声明表结构、事务、级联等,以及很棒的TS支持。

同时支持ORM的Data Mapper与Actice Record模式,具体参考:https://typeorm.biunav.com/zh/active-record-data-mapper.html#%E4%BB%80%E4%B9%88%E6%98%AFdata-mapper%E6%A8%A1%E5%BC%8F

具体用法参考:https://docs.nestjs.com/techniques/database

Prisma

Prisma对自己的定义仍然是NodeJS的ORM,但个人感觉它比普通意义上的ORM要强大得多。这里放一张官方的图,来大致了解下Prisma和ORM、SQL、Query Builder的能力比较:

img

具体用法参考:https://docs.nestjs.com/recipes/prisma和https://blog.logrocket.com/how-to-use-nestjs-prisma/

SQL 构造器

除了ORM与原生SQL以外,还有一种常用的数据库交互方式:Query Builder(以下简称QB)。

QB和ORM既有相同之处又有不同之处,比如MQuery (MongoDB的一个Query Builder)的方法是这样的:

1
2
3
4
5
6
7
8
9
10
mquery().find(match, function (err, docs) { 
assert(Array.isArray(docs));
})
mquery().findOne(match, function (err, doc) {
if (doc) {
// the document may not be found
console.log(doc);
}
})
mquery().update(match, updateDocument, options, function (err, result){})

看起来是和ORM很像,我们看看其他场景:

1
2
3
4
5
6
7
8
9
10
mquery().find(match, function (err, docs) { 
assert(Array.isArray(docs));
})
mquery().findOne(match, function (err, doc) {
if (doc) {
// the document may not be found
console.log(doc);
}
})
mquery().update(match, updateDocument, options, function (err, result){})

在ORM中,通常不会存在这样的多个方法链式调用,而是通过单个方法+多个参数的方式来操作,这也是Query Builder和ORM的一个重要差异。再来看看TypeORM的Query Builder模式:

1
2
3
4
5
6
7
import { getConnection } from "typeorm"; 
const user = await getConnection()
.createQueryBuilder()
.select("user")
.from(User, "user")
.where("user.id = :id", { id: 1 })
.getOne();

以上的操作其实就相当于userRepo.find({ id: 1 }),你可能会觉得QB的写法过于繁琐,但实际上这种模式要灵活的多,和SQL语句的距离也要近的多(你可以理解为每一个链式方法调用都会对最终生成的SQL语句进行一次操作)。

同时在部分情境(如多级级联下)中,Query Builder反而是代码更简洁的那一方,如:

1
2
3
4
5
6
7
8
const selectQueryBuilder = this.executorRepository 
.createQueryBuilder("executor")
.leftJoinAndSelect("executor.tasks", "tasks")
.leftJoinAndSelect("executor.relatedRecord", "records")
.leftJoinAndSelect("records.recordTask", "recordTask")
.leftJoinAndSelect("records.recordAccount", "recordAccount")
.leftJoinAndSelect("records.recordSubstance", "recordSubstance")
.leftJoinAndSelect("tasks.taskSubstance", "substance");

以上代码构建了一个包含多张表的级联关系的Query Builder。

级联关系如下:

  • Executor
  • tasks -> Task
    • relatedRecord -> Record
  • Task
    • substances -> Substance
  • Record
    • recordTask -> Task
    • recordAccount -> Account
    • recordSubstance -> Substance

knext.js

目前主流的QB是knext.js, Bookshelf.jsObjection.js的QB都是基于此来实现的,我们看下其具体使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const knex = require('knex')({
client: 'sqlite3',
connection: {
filename: './data.db',
},
});

try {

await knex.schema
.createTable('users', table => {
table.increments('id');
table.string('user_name');
})
.createTable('accounts', table => {
table.increments('id');
table.string('account_name');
table
.integer('user_id')
.unsigned()
.references('users.id');
})

const insertedRows = await knex('users').insert({ user_name: 'Tim' })

await knex('accounts').insert({ account_name: 'knex', user_id: insertedRows[0] })

const selectedRows = await knex('users')
.join('accounts', 'users.id', 'accounts.user_id')
.select('users.user_name as user', 'accounts.account_name as account')

const enrichedRows = selectedRows.map(row => ({ ...row, active: true }))

} catch(e) {
console.error(e);
};

可以看到knex的链式操作更进了一步,甚至可以链式创建多张数据库表。

Nest官方也提供了动态模块的方式来实现Nest项目内Knext的使用,具体参考:https://dev.to/nestjs/build-a-nestjs-module-for-knex-js-or-other-resource-based-libraries-in-5-minutes-12an和https://github.com/nestjsplus/knex以及https://github.com/nestjsplus/knex-cats

趋势

prisma官网有对2022年前11的DB查询工具的活跃度介绍,总结的较为全面,这里也贴一下当前介绍的几种ORM和QueryBuilder的NPM Trending

总结:prisma近几年越来越被开发者接受,社区活跃度高;TypeORM做为Nest官方推荐的ORM框架和TS支持,也有很高的下载量;Sequelize做为老牌的ORM下载量最大,且趋于稳定,但社区活跃度不高。