likes
comments
collection
share

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案

作者站长头像
站长
· 阅读数 26

项目背景

当前在项目中使用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类型的数据值产生了如下变化:

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案

javascript对象转换为json后的结果发生了改变,该字段原始javascript对象为:

{
  version: '1.0.0',
  data: Buffer.from('hello world'),
}

初步判断,升级后js对象的序列化逻辑发生了改变,导致Buffer在json中的表示产生了不一致,在升级后,Buffer转换为了base64字符串数据表示。

问题探查

  1. 创建prisma-debug项目,分别安装4.16.2和5.1.1版本,省略创建过程,只说主要部分,源码已放到mobiusy/prisma-debug (github.com)
    • 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();
      
  2. 分别在4.16.2和5.1.1版本中执行以上代码,复现了之前的问题,数据库中archiveSnapshot字段的值产生了不一致的情况
  3. 打开prisma:debug, 查看运行输出,在Generated request这一步打印的结果来看,数据有了差异

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案 4. 下载prisma源码 prisma/prisma

git clone git@github.com:prisma/prisma.git
cd prisma
git checkout 5.1.1

找到源码中对应的代码:

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案 源文件路径:github.com/prisma/pris…

顺着serializeJsonQuery方法一路往下找,最终定位到具体的序列化逻辑

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案

其中有一个逻辑判断ArrayBuffer.isView(jsValue), 是判断当前字段是否为ArrayBuffer的视图,解释见ArrayBuffer.isView() - JavaScript | MDN (mozilla.org), 当jsValue为Buffer时满足条件,把jsValue转换为base64字符串

  1. 至此问题已经比较清晰了,是升级后的jsonProtocol与原来的GraphQLProtocol对于js对象中Buffer字段的序列化实现不同产生的

解决问题

有两种方案可以解决:

  1. 把数据库中原来旧数据转换为新的格式存储
  2. 此问题是由于json对象不支持Buffer, Buffer序列化后产生的问题,因此把Buffer数据转换为json支持的数据类型再进行存储就可以

1方案需要清洗数据库,尽量不考虑这种方案 2方案只需要把Buffer数据转换为同4.16.2版本里序列化后的结果一致的输出即可,因此采用方案2

代码改动:

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案

复盘

此问题的直接原因是json序列化器的对Buffer数据的处理逻辑不同导致的,而问题最终在prisma版本升级后暴露出来,其实在最初的版本的业务代码里就埋下了隐患,我们回头看最初的业务实现:

Prisma ORM 4.x升级5.x遇到不兼容更新,源码探查解决方案

这里强制做了类型定义以满足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 | null> {}

InputJsonValue类型不接受Buffer类型,因此在最初的实现里通过as unknown as Prisma.InputJsonValue忽略静态检查警告为当前的问题埋下了伏笔。

总结

  1. 在typescript中的代码静态检查不要随意忽略
  2. 对于js对象中包含Buffer类型的字段,最好使用自己的序列化器,进行序列化,而不要依赖第三方内置的序列化器