项目背景
当前在项目中使用Prisma作为后端服务的ORM, Prisma ORM自称”Next-generation Node.js and TypeScript ORM“,支持的数据库有:
- PostgreSQL
- MySQL
- SQLite
- MongoDB
- CockroachDB
- Microsoft SQL Server
其他信息:
- 后端框架: NestJS
- 数据库: PostgreSQL
为什么升级5.x
2023/07/12官方发布了Prisma 5.0.0版本,see release note, 概况下来, 升级的主要内容有:
- 一些预览特性GA, 包括jsonProtocol, fieldReference, extendedWhereUnique, 因此如果在4.x本版里开启过这些特性,升级后可以把显示的开启声明移除掉
- 通用改进及一些破坏性改动
- 依赖最低要求版本Nodejs(16.13.0), Typescript(4.7), PostgreSQL(9.6)
- 主要变化
- 删除rejectOnNotFound属性
- 删除一些数组快捷方式
- cockroachdb提供程序现在是连接到CockroachDB数据库时所需的
- 已删除生成的Prisma客户端中的runtime/index.js文件
- 其他变化
- 删除Prisma CLI中的过时标志
- 删除库引擎中的beforeExit钩子
- 删除已弃用的prisma2可执行文件
- 删除Prisma模式中的已弃用的experimentalFeatures生成属性
- 重命名migration-engine为schema-engine
其中jsonProtocol正式GA,也是此次升级最大的亮点,升级后,Prisma将基于JSON的网络协议作为与查询引擎通信时使用的默认协议。 其实,在版本4.11.0中就引入了这一预览功能,以提高Prisma的性能。此前,Prisma使用类似GraphQL的协议与查询引擎进行通信。对于拥有较大模型的应用程序,CPU和内存消耗较高,这会导致性能瓶颈。 基于JSON的网络协议在Prisma客户端与查询引擎通信时提高了效率。
在2023/08/01发布的5.1.0版本里,进一步从SQL查询的实现层做了性能优化,具体见Release 5.1.0 · prisma/prisma (github.com)
所以决定升级到当前最新版本5.1.1, 为未来的5.x版本持续升级打下基础。
升级过程
- package.json依赖升级
"prisma": "^4.16.0"
=>"prisma": "^5.1.1"
"@prisma/client": "^4.16.0"
=>"@prisma/client": "^5.1.1"
- prisma shcema中移除GA的预览特性
- 重新生成client
npx prisma generate
兼容性问题出现
prisma升级后,服务运行正常,在开发环境运行一段时间后,测试反馈数据产生异常,观察出现异常的数据,发现数据库中某个字段json类型的数据值产生了如下变化:
javascript对象转换为json后的结果发生了改变,该字段原始javascript对象为:
{
version: '1.0.0',
data: Buffer.from('hello world'),
}
初步判断,升级后js对象的序列化逻辑发生了改变,导致Buffer在json中的表示产生了不一致,在升级后,Buffer转换为了base64字符串数据表示。
问题探查
- prisma schema 定义
model Case { id String @id @default(uuid()) name String rawBinary Bytes archiveSnapshot Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
- 测试代码
import { Prisma, PrismaClient } from '@prisma/client'; const client = new PrismaClient({ log: ['query', 'info', 'warn', 'error'], }); async function write() { try { client.$connect(); await client.case.create({ data: { name: 'prisma', rawBinary: Buffer.from('hello world'), archiveSnapshot: { version: '1.0.0', data: Buffer.from('hello world'), } as unknown as Prisma.InputJsonValue, }, }); } catch (error) { console.error(error); } finally { client.$disconnect(); } } function main() { write(); } main();
archiveSnapshot
字段的值产生了不一致的情况
4. 下载prisma源码 prisma/prisma
git clone git@github.com:prisma/prisma.git
cd prisma
git checkout 5.1.1
找到源码中对应的代码:
源文件路径:github.com/prisma/pris…
顺着serializeJsonQuery
方法一路往下找,最终定位到具体的序列化逻辑
其中有一个逻辑判断ArrayBuffer.isView(jsValue)
, 是判断当前字段是否为ArrayBuffer的视图,解释见ArrayBuffer.isView() - JavaScript | MDN (mozilla.org), 当jsValue为Buffer时满足条件,把jsValue转换为base64字符串
解决问题
有两种方案可以解决:
1方案需要清洗数据库,尽量不考虑这种方案
2方案只需要把Buffer数据转换为同4.16.2版本里序列化后的结果一致的输出即可,因此采用方案2
代码改动:
复盘
此问题的直接原因是json序列化器的对Buffer数据的处理逻辑不同导致的,而问题最终在prisma版本升级后暴露出来,其实在最初的版本的业务代码里就埋下了隐患,我们回头看最初的业务实现:
这里强制做了类型定义以满足archiveSnapshot
字段的类型,那看一下archiveSnapshot的类型是:
archiveSnapshot: JsonNullValueInput | InputJsonValue
export type InputJsonValue = string | number | boolean | InputJsonObject | InputJsonArray
export type InputJsonObject = {readonly [Key in string]?: InputJsonValue | null}
export interface InputJsonArray extends ReadonlyArray {}
InputJsonValue
类型不接受Buffer类型,因此在最初的实现里通过as unknown as Prisma.InputJsonValue
忽略静态检查警告为当前的问题埋下了伏笔。
总结