数据库模块集成之ORM选择
1 )概述
- NestJS 本身和数据库并无直接关联,它的底层使用 Express 或者 Fastify 来提供 HTTP 接口服务
- 在数据库方面,NestJS 使用 Node.js 端的一些驱动,也就是 NestJS 端的数据库驱动来与数据库进行对接。
- 不过,它更推荐使用一些 ORM(对象关系映射)库来与数据库对接
- 主要原因在于,这些 ORM 库能够与各式各样的数据库进行对接,做到“开箱即用”(out of the box)
- 我们无需根据不同的驱动或者不同的数据库去定制不同的逻辑代码
- 官方指出,市面上有不少值得推荐的 ORM 库,像 MikroORM、Sequelize、Knex.js、TypeORM 和 Prisma 等
- 同时,官方还推出了几个中间件库,如 @nestjs/typeorm、@nestjs/mongoose 等
- 此时,我们就需要思考到底选用哪一款 ORM 库,哪一款在后续开发中能为我们节省大量工作
2 ) ORM 库的选择
-
NestJS 官方不推荐直接使用 Node.js 端的驱动,而是推荐使用 ORM 库,主要目的就是这些 ORM 库可以实现一个 ORM 对接多个数据库
-
就拿官方推荐的 TypeORM 来说,打开 TypeORM 官方网站,在左侧菜单有个“DataSource Options”,其中关于“type”的部分,明确写着 TypeORM 可以与多种类型的数据库对接,常见的有 MySQL、PostgreSQL、SQLite、MariaDB、Oracle 等,甚至还包括 MongoDB 等
-
另外,Prisma 也是非常推荐的,它同样能与各式各样的数据库对接。在 Prisma 的官方网站上,对于它支持的数据库类型和版本都有详细描述,包括 MongoDB、PostgreSQL、MySQL 以及 SQL Server 等,这些都是很常见的数据库
-
不同的 ORM 库之间是存在区别和侧重的,所以在选择时,有几个要点需要考虑,虽然 ORM 库是更高层面的抽象和封装,能让一套代码操作不同的数据库,可针对不同用户的应用场景进行数据库对接,比如有的用户购买了商业化授权的 Oracle 数据库,有的使用付费版的 PostgreSQL、MySQL,还有的使用免费的 MySQL,这些情况都有可能
-
因此,我们选择时要兼容最多的场景,同时还要考虑该 ORM 库在兼容这些场景后的稳定性、后续扩展性以及社区支持情况
-
这和我们平时挑选 NPM 上的第三方包的策略很相似,大家可以去相关 ORM 库的官方网站看看,也可以参考 NPM 上的下载量
-
很多 ORM库,比如:MicroORM 这类,像 Sequelize、TypeORM、Prisma,Mongoose
-
虽然严格来说,Mongoose 不算传统意义上的 ORM 库,但它是与 MongoDB 对接的优秀驱动
-
相比之下,Mongoose 在与 MongoDB 对接时表现出色。目前,TypeORM 仅支持 MongoDB 的部分版本,Prisma 支持 MongoDB 4.2 及以上版本
-
从官方给出的表格能看到,对于 MongoDB 版本要求是 4.2 以上。对于一些老系统,若使用的是 MongoDB 4.0 版本,Prisma 就无法支持,此时就必须使用 Mongoose,而且 Mongoose 支持的操作方法更全面
-
以 Prisma 官方介绍的“database features matrix”为例,它支持事务、索引等常见属性,但也有一些不支持的功能,比如聚合管道(aggregation pipeline),这在编写复杂的 MongoDB 查询时非常重要,Prisma 不支持这一点比较可惜
-
我们试图在 NestJS 官方推荐的这些 ORM 库中找到一个万能的 ORM 库,但目前来看,TypeORM 虽比较接近我们的使用场景,但在操作 MongoDB 方面,还是不如 Mongoose
-
不过,这两者对常见的关系型数据库支持都很好。在 TypeORM 官方网站搜索 MongoDB 会发现,它虽支持 MongoDB,但指定的是特定版本(experimental),而现在 MongoDB 已经更新到 7.x 版本,如果只能安装 5.x 版本的驱动,就无法对接 6.x、7.x 版本
-
有的同学可能想把 TypeORM 和较新的 MongoDB 版本集成,但搜索相关 issues 会发现有很多坑点,所以不建议这么做
-
如果使用 Mongoose 对接 MongoDB,就意味着需要维护两套代码,一套是 Mongoose 对应 MongoDB 的 Schema,另一套是 TypeORM 或 Prisma 的实体类,这就需要我们进行权衡考量
-
权衡点主要有:一是是否需要对接 MongoDB 数据库;二是对接的 MongoDB 数据库版本是多少;三是如果对接 MongoDB 数据库,是否需要使用其进阶特性。
-
如果不需要对接 MongoDB 数据库,或者只是可能会对接但不使用其进阶特性,那么可以考虑使用 Prisma,因为它能涵盖市面上绝大多数应用场景的数据库;如果觉得 Prisma 涵盖的场景不够,也可以考虑 TypeORM,但这会带来权限问题,因为它们定义数据库连接和使用的方式不同,可能需要在 ORM 库与数据库操作层面或逻辑层面写一层新的封装,封装常见的数据库操作方法,如根据 ID 查询、查询所有、删除、更新等
-
一般来说,我们的业务系统大多只对接一种类型的数据库,要么是关系型数据库,要么最多扩展到 Redis 这种非关系型数据库。不过,也有一些特殊场景可能需要对接多种数据库,比如跨系统升级时,需要从老旧系统读取数据;还有低代码类型项目,需要让用户使用自己本地不同版本的数据库,这时就需要 ORM 库的支持
-
后续我们会将 Prisma、TypeORM 以及 Mongoose 集成到我们自己的项目中,但在业务代码层面,不会同时使用这三者,主要有以下三点原因
- 第一,同时支持这三者代码量会非常大,因为 Prisma 需要定义 Schema,TypeORM 需要定义实体类,Mongoose 需要针对 MongoDB 设计对应的 Schema,且它们之间并不通用
- 第二,Prisma、Mongoose 和 TypeORM 官方文档都比较全面,我们后续的业务会基于其中一个方案进行扩展,如果需要扩展到其他方案,可以参考官方文档
- 第三,这三者支持的数据库各有侧重,在通用模块项目中,我们虽要考虑支撑市面上所有数据库类型,但不能本末倒置,要考虑投入产出比,因为我们的使用场景大多只对接一种数据库,即便考虑分布式数据库,也可能是同一版本的数据库
-
当我们既需要将数据存到关系型数据库,又需要存到像 MongoDB 这样的非关系型数据库时,官方文档给出了示例
-
在官方的 database 部分有一个“multiple database”示例,我们既可以引入 TypeORM,也可以扩展 Prisma 或者对接 MongoDB
-
这样一来,同一个项目中可能既存在 MongoDB 的 Schema,也存在 Prisma 的 Schema 和 TypeORM 的实体类,组织代码时就需要格外慎重,以免混淆
-
在 NestJS 里连接数据库的方式和形式非常丰富,我们既可以在一个 NestJS 应用里对接多个数据库,甚至是多个不同类型的数据库
-
这就是我们关于在通用模块项目中集成数据库模块的建议,我们后续对接数据库时,要明确自己的目标,项目是为了支撑后续业务开发,而不是纠结于覆盖所有数据库类型
NestJS 和 ORM 之间的关系
- ORM 库是 NestJS 与数据库之间的桥梁,这里挑选了三个相对典型的 ORM 库
- 对于 TypeORM 方案来说
- 它支持的关系型数据库非常丰富,但对非关系型数据库的支持不太友好
- 对于 Prisma 方案来说
- 而 Prisma 操作数据库层面很方便,不过对关系型数据库的支持不够丰富
- 对于 Mongoose 方案来说
- 对非关系型数据库支持得非常好
在 NestJS 中应选择的哪种方案
- 如果不谈业务只谈技术,那就是耍流氓。就像乔布斯所说,做产品要从业务需求出发
- 所以在生产过程中,大家在这三者中做选择时,不用过于纠结
- 因为根据自身业务场景,答案可能就已经明确了
- 比如,若只是使用开源的 MySQL 方案,加上 TypeORM 或许就能解决问题
- 若对 MySQL 没有版本需求,团队开发时对数据库操作层面没有精细化要求
- 且需要方便的操作,那么 Prisma 就是首选
1 )TypeORM
- 如果团队需要对接多种关系型数据库,其中还有客户指定的特殊数据库,或者有较旧的 MySQL 版本需要对接,那么毫无疑问应选择 TypeORM
- 因为它对关系型数据库的支持非常全面,即使是较低版本也能很好地支持,其官方文档全面,生态也最完善
- 在处理一对多、多对多关系,或是操作关键表时,可能会纠结关联字段和 ID 该放在哪一边,TypeORM 都有完善的文档可参考
- 而且,若不想使用它的官方方法,还能使用原生 SQL 语句操作数据库,从这些方面来看,它无疑是 ORM 库中的“老大哥”
2 ) Prisma
- Prisma 主要针对习惯写 TS 的小伙伴,它简化了数据库层面的操作,开发了 Prisma Client, 其中封装了数据库操作
- 理解这些关键细节后,再看 TypeORM 以及 Prisma 官方对它们的介绍和代码,结合业务目的,就不会那么纠结了
3 )单库 和 多库的选择
- 如果每天的数据量不大,在十万以内,选择单库方式就可以,那什么情况下选择多库,什么情况下对接多种数据库呢?
- 有时候,对一种数据库(如 MySQL)进行读写分离,设置主从数据库即可,通常写操作是数据库的瓶颈,读操作则不是
- NestJS 能很好地支持对接一种数据库的多库,带着这样的问题去看 NestJS 的官方文档,就不会对代码和技术方案感到陌生和堵心了
- NestJS 是一个大而全的框架,涵盖了所有服务端开发的应用场景,大家可以针对自身业务在其生态中找到解决方案
- 做技术的小伙伴要记住,学习和使用技术是为业务服务的,我们选择技术方案是对技术复杂度提升后应对的场景
- 多库场景,可以定义多个数据库并命名,后续操作时用依赖注入的方式选择操作哪个数据库,例如,一个数据库用于写操作,另一个用于读操作
- 读写分离通常能解决性能问题,主数据库一般用于写,从数据库用于读,数据库之间的同步问题通常可由数据库层面解决。若网络状况良好,同步问题基本能得到解决
- 数据库集群一般依靠外部工具(如 K8s),通过容器化方式快速启动多个数据库实例,再加上数据库之间的同步和负载均衡,就能承载较大的业务系统
单库和多库优缺点
- 在具体业务场景中,可针对某一种 ORM 库做具体选择,绝大多数场景是单库单 ORM
- 下面分析下单库和多库的优缺点和应用场景。
单库
- 优点:适用于对成本敏感、数据承载量不大、安全性要求不高的场景,成本较低。常用于小程序、公众号等需要快速开发和部署的应用场景
- 缺点:可能受物理机硬件限制,导致性能和扩展性受限。若出现故障,存在数据丢失风险
多库
-
应用场景:
- 对自定义需求非常高,不同用户要求使用不同数据库(如 MySQL、MongoDB、Oracle 或 SQL Server)。
- 一种数据库多库,可考虑用一个 ORM 对接
- 大型业务场景,或对合规和法规要求严格,必须进行物理隔离时,需用多个 ORM 对接多种数据库
-
优点:能满足复杂业务需求,不同类型数据库发挥各自优势。
-
缺点:架构变得复杂,需了解多种数据库的操作、部署和维护方式
- 有些数据库在本地
- 有些在用户侧,尤其是低代码项目和数据敏感项目
- 此外,还可能出现数据一致性问题,分布式读写需要加入事务处理,这较为复杂,且不同数据库对事务的支持情况不同
- 如 MongoDB 在 4 版本之后才支持事务,老旧项目使用旧版本时需考虑数据库升级和迁移
通用框架,如何考虑涵盖单库和多库这两种场景
- 第一步了解 NestJS 如何与这三个 ORM 库做基础对接, 第二步再深入探讨如何将它们融合到框架中
- 以业务场景为例,探讨如下:
单、多库的选择
- 如日记系统需分成多个租户,每个租户用单独字段做逻辑隔离,安全性要求不高、数据量适中,可使用单库场景
- 若用户安全性要求高,需将素材采集的图片数据存放到自己的数据库,就采用多库场景
关系、非关系型 数据库选择
- 通常,文件操作对非关系型数据库(如 MongoDB)友好,素材设备采集的动态数据也适合用非关系型数据库存储
- 而用户管理系统等内容用关系型数据库更好