Prisma 数据导入导出实战:基于 NDF 的备份、迁移与克隆完整指南

原创2026-09-21 21:30:351,666 阅读
文章标签:后端数据库GraphQL

Prisma 数据导入导出实战:基于 NDF 的备份、迁移与克隆完整指南

Prisma 通过一种专用的中间数据格式——Normalized Data Format(NDF)——来承载所有数据的导入与导出操作。本文以官方 Database Workflows 教程(见 docs/1.10/03-Tutorials2/07-Database-Workflows/01-Data-Export-&-Import.md)为主线,完整演示"创建服务 → 写入种子数据 → 导出 NDF → 部署新 stage → 导入 NDF"的全流程,并结合本仓库 CLI 源码(prisma-cli-core 中的 Import/Export 命令实现)深入讲解 NDF 的结构规范、prisma export/prisma import 的底层原理(游标分页、断点续传、数据校验)以及原始 Import/Export API 的调用方式。读完本文,你将能够独立完成 Prisma 服务的数据备份、数据库克隆与跨 stage 数据迁移。

数据流动:SQL / MongoDB / JSON → NDF → Prisma

在 Prisma 中,导入与导出始终以 NDF 为"中间媒介"。整体数据流如下:

+--------------+                    +----------------+                       +------------+
| +--------------+                  |                |                       |            |
| |            | |                  |                |                       |            |
| | SQL        | |  (1) transform   |      NDF       |  (2) chunked upload   |   prisma   |
| | MongoDB    | | +--------------> |                | +-------------------> |            |
| | JSON       | |                  |                |                       |            |
| |            | |                  |                |                       |            |
+--------------+ |                  +----------------+                       +------------+
  +--------------+

两个关键步骤中:

  1. (1)transform:把 MySQL、MongoDB、JSON 等具体数据源转换为 NDF。根据 docs/1.10/04-Reference/06-Data-Import-&-Export/01-Data-Import.md 的说明,这一步目前需要手动完成(仓库文档明确标注"not available yet",CLI 尚不支持直接从 MySQL/MongoDB/Firebase 导入)。
  2. (2)chunked upload:把 NDF 数据分批上传到 Prisma 服务,既可以通过 prisma import CLI 命令,也可以调用原始的 /import HTTP API。

教程:完整走一遍数据导出与导入

以下步骤将创建一个小型 Prisma 服务(包含一对 User–Post 关系),写入种子数据后导出为 NDF,再部署到新的 stage 并导入数据,最终得到一个数据完全相同的"克隆"服务。

第一步:创建 Prisma 服务

在任意目录下执行:

prisma init import-example

当 CLI 询问使用哪种模板时,选择 Minimal setup: database-only。命令会生成一个名为 import-example 的新目录,其中包含根配置文件 prisma.yml 与数据模型定义文件 datamodel.graphql。

接下来更新数据模型,加入一个一对多关系。将 datamodel.graphql 的内容改为:

type User {
  id: ID! @unique
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID! @unique
  title: String!
  author: User!
}

第二步:用 seed 机制写入初始数据

在 import-example 目录中新建 seed.graphql,写入以下 mutation(创建一个名为 Sarah 的用户及三篇关联文章):

mutation {
  createUser(data: {
    name: "Sarah",
    posts: {
      create: [
        { title: "GraphQL is awesome" },
        { title: "It really is" },
        { title: "How to GraphQL is the best GraphQL tutorial" }
      ]
    }
  }) {
    id
  }
}

然后修改 prisma.yml,通过 seed 属性告诉 CLI 这个文件的存在:

service: import-example
stage: dev

datamodel: datamodel.graphql

# to enable auth, provide
# secret: my-secret
disableAuth: true

seed:
  import: seed.graphql

执行部署,让 CLI 在部署服务时自动执行 seed.graphql 中的 mutation:

prisma deploy

当 CLI 询问部署到哪个 cluster 时,从公共集群中选择 prisma-eu1 或 prisma-us1(教程注明:使用 Docker 本地部署时 seeding 同样生效)。

部署完成后,打开 GraphQL Playground 发送以下查询即可验证数据已写入:

{
  users {
    name
    posts {
      title
    }
  }
}

预期返回:

{
  "data": {
    "users": [
      {
        "name": "Sarah",
        "posts": [
          { "title": "GraphQL is awesome" },
          { "title": "It really is" },
          { "title": "How to GraphQL is the best GraphQL tutorial" }
        ]
      }
    ]
  }
}

第三步:以 NDF 导出数据

在 import-example 目录执行:

prisma export

该命令会生成名为 export-__TIMESTAMP__.zip 的文件(__TIMESTAMP__ 为导出时刻的时间戳),压缩包内的文件即为 NDF 格式。NDF 的详细结构规范见 Normalized Data Format 参考文档。

从源码看,默认导出文件名正是由时间戳生成的:在 cli/packages/prisma-cli-core/src/commands/export/index.ts 中,exportPath 默认为 export-${new Date().toISOString()}.zip,且若路径不以 .zip 结尾会自动补上后缀。

第四步:部署到新的 stage

现在创建服务的"克隆":打开 prisma.yml,将 stage 改为新值,并移除 seed 与 cluster 属性:

service: import-example
stage: test

datamodel: datamodel.graphql

# to enable auth, provide
# secret: my-secret
disableAuth: true

再次执行 prisma deploy 部署到新的 test stage(cluster 仍选 prisma-eu1 或 prisma-us1)。此时 test 服务是全新的、空的。

第五步:导入 NDF 数据

使用 prisma import 将之前导出的 zip 数据导入新服务。注意将 __DATA__ 替换为导出文件的真实路径(例如 export-2018-01-13T19:28:25.921Z.zip):

prisma import --data __DATA__

导入完成后,为当前 test stage 打开 GraphQL Playground,再次发送上面那条 users 查询,即可看到 Sarah 及其三篇文章——数据成功迁移到了新 stage。

深入 NDF:三种 value type 与文件结构

NDF 是描述 JSON 结构的一种规范,把数据按三种 value type 拆分:

  • Nodes:节点标量字段的数据(即数据库记录本身)
  • Lists:节点的列表字段(scalar list)数据
  • Relations:通过关系字段把两个节点连接起来的数据

每个 NDF JSON 文档是一个包含两个键的对象:

  • valueType:取值 "nodes"、"lists" 或 "relations"
  • values:符合对应 value type 的数据数组

以下示例均基于这个数据模型:

type User {
  id: String! @unique
  firstName: String!
  lastName: String!
  hobbies: [String!]!
  partner: User
}

nodes

结构为 { "_typeName": STRING, "id": STRING, "<scalarField1>": ANY, ... }。_typeName 指向数据模型中的 SDL 类型名,<scalarFieldX> 是该类型的标量字段名。例如导入两个 User 节点的标量值:

{
  "valueType": "nodes",
  "values": [
    {"_typeName": "User", "id": "johndoe", "firstName": "John", "lastName": "Doe"},
    {"_typeName": "User", "id": "sarahdoe", "firstName": "Sarah", "lastName": "Doe"}
  ]
}

lists

结构为 { "_typeName": STRING, "id": STRING, "<scalarListField>": [ANY] }。与标量字段不同,每个对象只能为列表字段提供值。例如导入两个 User 节点的 hobbies 列表字段:

{
  "valueType": "lists",
  "values": [
    {"_typeName": "User", "id": "johndoe", "hobbies": ["Fishing", "Cooking"]},
    {"_typeName": "User", "id": "sarahdoe", "hobbies": ["Biking", "Coding"]}
  ]
}

relations

结构与前两者不同:values 数组中的每个元素本身是一对节点(固定包含两个元素的数组),用于描述一条关系。例如通过 partner 关系字段连接 johndoe 与 sarahdoe:

{
  "valueType": "relations",
  "values": [
    [
      { "_typeName": "User", "id": "johndoe", "fieldName": "partner" },
      { "_typeName": "User", "id": "sarahdoe", "fieldName": "partner" }
    ]
  ]
}

目录与文件命名约定

当通过 CLI 导入目录(或 zip)时,NDF 文件必须按类型放入同名目录 nodes、lists、relations 中;文件必须是 .json 后缀,并按从 1 开始的递增序号命名,允许前导零,如 1.json、01.json 或 0000001.json(见 Data Import 文档)。典型结构如下:

.
├── data
│   ├── lists
│   │   ├── 0001.json
│   │   ├── 0002.json
│   │   └── 0003.json
│   ├── nodes
│   │   ├── 0001.json
│   │   └── 0002.json
│   └── relations
│       └── 0001.json
├── datamodel.graphql
└── prisma.yml

随后执行 prisma import --data data 即可导入整个 data 目录。

CLI 命令参考与实现原理

prisma export

在 cli/packages/prisma-cli-core/src/commands/export/index.ts 中,prisma export 支持的 flags 如下:

Flag 短写 说明
--path -p 导出 zip 文件的保存路径;缺省时自动生成 export-<ISO 时间戳>.zip
--env-file -e 注入环境变量的 .env 文件路径
--project — Prisma 定义文件(prisma.yml)的路径

导出完成后 CLI 还会提示下一步命令:prisma import --data <exportPath>。

底层实现(Exporter.ts)可以总结为三步:

  1. 按类型分页下载:依次下载 nodes、lists、relations 三类数据。每次请求携带一个四元组游标 cursor: { table, row, field, array }(初始全部为 0),服务端返回数据与新的游标;客户端循环请求直到游标四项之和小于 0 为止,即"游标耗尽"。
  2. 分片写盘:每批数据写入 .export/<fileType>/ 目录下、按序号命名的 JSON 文件(000001.json 起,6 位前导零),文件内容为 { "valueType": <fileType>, "values": [...] }。
  3. 打包压缩:用 archiver 将整个 .export 目录打成 zip,随后清理临时目录。

prisma import

在 cli/packages/prisma-cli-core/src/commands/import/index.ts 中,prisma import 支持的 flags 如下:

Flag 短写 说明
--data -d 必填。包含 NDF 数据的 zip 文件或目录路径
--env-file -e 注入环境变量的 .env 文件路径
--project -p Prisma 定义文件(prisma.yml)的路径

命令入口会做两类预检查:--data 指向的路径必须存在;且该路径必须以 .zip 结尾或是一个目录,否则直接报错。CLI 命令的完整参考见 prisma import 命令文档。

底层实现(Importer.ts)的导入管线为:

  1. 解压:若是 zip 文件,用 adm-zip 解压到 .import/ 临时目录。
  2. 校验:通过 Validator.ts 对三类文件逐一做结构校验——要求存在 values 数组、节点必须带 _typeName 与字符串类型的 id、缺失/多余字段会报错、标量值按 ID/String/Int/Float/DateTime/Boolean/Json 及数据模型中枚举进行类型检查。
  3. 分类型上传:按 nodes → lists → relations 的顺序逐文件调用上传接口,每个文件上传成功后更新 state.json 记录进度;异常时打印错误并清理临时目录。

值得强调的是,state.json 的存在使得导入可中断并续传:若导入中途失败,重新执行 prisma import 会跳过已导入的文件(日志输出 Skipping file ... (already imported))。此外,导入操作不是幂等的——重复导入总是新增数据而不会更新已有节点,同一 id 的节点被导入多次会导致未定义行为,可能损坏服务(见 Data Import 文档 的警告框)。

直接调用原始 Import / Export API

除 CLI 外,还可以直接调用 HTTP API。/export 与 /import 端点位于服务 HTTP 地址之下,例如 http://localhost:60000/my-app/dev/export 或 https://database.prisma.sh/my-app/prod/import。单次请求传输的 NDF JSON 数据上限为 10 MB,且必须携带 JWT 认证 token(放在 HTTP Authorization: Bearer ... 头中)。

原始导出 API

向 /export 发送 POST 请求,body 为:

{
  "fileType": "nodes",
  "cursor": {
    "table": 0,
    "row": 0,
    "field": 0,
    "array": 0
  }
}

cursor 描述从数据库何处开始导出。每次响应都会返回一个新游标,其状态有两种(详见 Data Export 文档):

  • 已终止:table、row、field、array 全部为 -1,表示导出完成。
  • 未终止:任一值不是 -1,表示已达 10 MB 上限,应把返回的游标作为下一次请求的输入继续导出。

curl 示例:

curl 'http://localhost:60000/my-app/dev/export' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer __JWT_AUTH_TOKEN__' \
-d '{"fileType":"nodes","cursor":{"table":0,"row":0,"field":0,"array":0}}' \
-sSv

原始导入 API

向 /import 发送 POST 请求,body 为 NDF JSON(valueType 取 nodes / lists / relations)。curl 示例:

curl '__SERVICE_ENDPOINT__/import' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer __JWT_AUTH_TOKEN__' \
-d '{"valueType":"nodes","values": __DATA__ }' \
-sSv

需要注意的是:原始 import API 不做任何数据校验,导入非法数据会导致未定义行为;只有走 CLI 时才会执行前述 Validator 的基础校验。官方给出的最佳实践是:以"相同数据模型的服务上做一次 export"得到的数据作为参考,来确保手工构造的导入数据合法。

注意事项与边界

围绕本仓库代码与文档,以下几点对实际使用尤为关键:

  • 文件大小建议:通过 CLI 导入时,每个 NDF 文件建议不超过 1 MB,否则可能触发超时(见 Data Import 文档);直接调 API 时单请求上限为 10 MB。
  • 文档型数据库暂不支持:在 export/index.ts 与 import/index.ts 中,若服务配置的 databaseType 为 document(MongoDB),CLI 会直接报错,提示使用 MongoDB 原生的 mongodump / mongorestore。
  • 手动转换是必经环节:把 SQL / MongoDB / JSON 数据转换成 NDF 目前没有自动化工具,需要按 NDF 规范手工构造 JSON。
  • 命令维护状态:CLI 源码中的输出警告明确说明 prisma export / prisma import 命令后续不再继续开发,建议关系型数据库(MySQL、PostgreSQL)的数据迁移改用各自的原生工具(如 mysqldump / mysqlimport、pg_dump / pg_restore)。在当前版本中,这两个命令仍可正常完成 NDF 层面的备份、迁移与 stage 克隆工作。
  • 同一 schema 才能互导:NDF 数据只能导入到数据模型一致的服务中,这也是"克隆服务"场景(如向 dev stage 灌入测试数据)最推荐的使用方式。
登录后查看全文
prisma1