首页 / Prisma ORM 入门教程 / 特殊字段类型:JSON、标量列表、Decimal 与 null/undefined

Prisma ORM 入门教程

特殊字段类型:JSON、标量列表、Decimal 与 null/undefined

本教程共 54 篇 · 第 38 篇 · 更新于 2026-08-11 · 约 7 分钟阅读

JSON 字段JsonNull标量列表DecimalBigIntnull 与 undefined复合 IDstrictUndefinedChecks

本节目标:摸清 JSON、数组、Decimal 和空值的脾气,写查询不再踩坑。

普通字段好懂,特殊字段各有各的脾气。JSON 有空值语义的坑,数组有过滤的坑,Decimal 有序列化的坑。逐一过一遍。

JSON 字段:读写与过滤

什么时候该用 JSON?两种典型:数据结构不固定,比如用户自定义配置、埋点事件;或者从外部系统导入的数据,不想一一映射成模型。结构固定、要按字段查的数据,还是建关系表更合适。

模型里声明 Json 字段,写入时直接传对象或数组:

await prisma.user.create({
  data: {
    email: "alice@example.com",
    metadata: { plan: "pro", flags: ["beta"], score: 42 },
  },
});

读取时整个对象返回。想按类型处理嵌套内容,用 Prisma.JsonArrayPrisma.JsonObject 工具类做收窄:

const pets = user.metadata as Prisma.JsonArray;

过滤分两层。整字段精确匹配用 equals / not;钻到内部用 path 加条件。path 语法两个数据库不一样,PostgreSQL 用数组,MySQL 用 $ 路径:

// PostgreSQL
await prisma.user.findMany({
  where: {
    metadata: { path: ["plan"], equals: "pro" },
  },
});

// MySQL
await prisma.user.findMany({
  where: {
    metadata: { path: "$.plan", equals: "pro" },
  },
});

嵌套对象就继续往下写路径,PG 是 ["pet2", "petName"],MySQL 是 "$.pet2.petName"。字符串和数组还有专门的操作符:string_contains(可配 mode: “insensitive” 忽略大小写)、array_contains。注意 PG 的 array_contains 必须传数组,哪怕只有一个值:

// PostgreSQL
where: { metadata: { path: ["flags"], array_contains: ["beta"] } }
Note

JSON 更新是整体覆盖。只改一个键,要么读出来改完写回,要么用原始查询 $executeRaw 调 PostgreSQL 的 jsonb_set 原位修改。频繁改局部字段的场景,考虑把字段拆成普通列。

Schema 里可以给 JSON 字段设默认值,注意要带引号包裹:

model User {
  metadata Json @default("{}")
}

两个已知限制要知道:JSON 不能只返回其中几个键,返回的是整个对象;也不能直接过滤「某个键是否存在」。有这两种需求,先用原始查询顶一下,或考虑换建模方式。

JsonNull 与 DbNull:两种空值

SQL 数据库里 JSON 字段的空有两种:数据库层的 NULL,和 JSON 值本身是 null。两者在数据库里完全不同。Prisma 用三个枚举区分:

枚举含义
Prisma.JsonNull存入 JSON 字面量 null
Prisma.DbNull存入数据库 NULL
Prisma.AnyNull过滤时匹配两者任意一种
await prisma.log.create({ data: { meta: Prisma.JsonNull } }); // JSON null
await prisma.log.create({ data: { meta: Prisma.DbNull } });   // 数据库 NULL

await prisma.log.findMany({
  where: { meta: { equals: Prisma.AnyNull } },
});

写 null 用错了枚举,语义就变了:{ meta: null } 这种裸写会被当成数据库 NULL,存进去的是 NULL 而不是 JSON 的 null。过滤空值同样不能省掉 equals 简写。

Note

MongoDB 没有这种区分,但 v7 不支持 MongoDB,相关需求留 v6.19。

标量列表:数组字段

PostgreSQL 支持真正的数组列。创建时直接给数组,追加用 push,整体替换用 set:

await prisma.user.create({
  data: { email: "bob@example.com", tags: ["prisma", "orm"] },
});

await prisma.user.update({
  where: { id: 1 },
  data: { tags: { push: "typescript" } }, // 仅 PostgreSQL / CockroachDB / MongoDB
});

过滤操作符:has 含某个值、hasEvery 全部包含、hasSome 至少包含其一、isEmpty 为空数组:

await prisma.post.findMany({
  where: {
    tags: { hasEvery: ["databases", "typescript"] },
  },
});
Warning

关系型数据库里,数组为 NULL 的记录不会被 NOT 和 isEmpty 匹配到。比如查「tags 不含 databases」,NULL 数组的记录不会返回。给数组字段加 @default([]) 从源头规避。

数组过滤还有一个细节:PG 的 array_contains 传多个对象时,要求全部对象都在数组里才算匹配,不是「至少一个」。传字符串会被转义导致查不到,务必传对象数组。

MongoDB 专属的 unset 操作符 v7 不支持,留 v6.19。

Decimal、BigInt 与 Bytes

金额类字段用 Decimal,别用 Float。浮点误差会算错钱,0.1 + 0.2 在二进制浮点里不是 0.3。Decimal 由 Prisma.Decimal 表示(底层是 decimal.js),支持精确运算:

import { Prisma } from "./generated/prisma/client";

await prisma.order.create({
  data: { amount: new Prisma.Decimal("19.99").plus(0.01) },
});

BigInt 映射为 TS 的 bigint,适合超大的整数,比如雪花 ID、大额计数。它有个著名的坑:直接 JSON.stringify 会抛错:

JSON.stringify({ revenue: 100n });
// TypeError: Do not know how to serialize a BigInt

给 stringify 传一个 replacer,把 bigint 转成字符串:

JSON.stringify(data, (_key, value) =>
  typeof value === "bigint" ? value.toString() : value,
);

Bytes 字段映射为 Uint8Array,适合存哈希、密钥、二进制内容,写入时 new Uint8Array([1, 2, 3]) 即可。

null 与 undefined:别混用

两条铁律:

  1. null 是值。写入会把字段置为 NULL,过滤匹配 IS NULL。
  2. undefined 表示「不做任何事」。对应的键会被整个忽略。

后果很实在。update 时把可选字段传 null,字段真的会被清空;传 undefined 则保持原值。动态表单更新时,没填的字段一定要传 undefined,不能传 null。反过来,过滤时 { field: null } 查 IS NULL,{ field: undefined } 等于没写这个条件。还有一个社区经验(未见于官方文档):OR 数组里的 undefined 条件会被保留并导致查不到数据,AND / NOT 里则会被忽略。GraphQL 这类接口里 null 和 undefined 常常混着来,落库前要统一转换成 undefined 再交给 Prisma,否则用户没填的字段会被清空。

这个行为容易误伤。v7 提供 strictUndefinedChecks 预览功能(Preview):显式传 undefined 直接报错,想跳过字段用 Prisma.skip

generator client {
  provider        = "prisma-client"
  output          = "../src/generated/prisma"
  previewFeatures = ["strictUndefinedChecks"]
}
await prisma.user.create({
  data: {
    name: "Alice",
    email: optionalEmail ?? Prisma.skip,
  },
});

再配合 tsconfig 的 exactOptionalPropertyTypes,编译期就能抓住 undefined 乱传的问题。两个一起开,空值相关的 bug 基本绝迹。

复合 ID

多个字段共同组成主键,用 @@id

model Like {
  postId Int
  userId Int

  @@id([postId, userId])
}

复合 ID 在 where 里以嵌套对象出现。可以给复合 ID 命名,查询时直接用名字:

model Like {
  postId Int
  userId Int

  @@id(name: "likeId", fields: [postId, userId])
}
await prisma.like.findUnique({
  where: {
    likeId: { userId: 1, postId: 1 },
  },
});

await prisma.like.update({
  where: { likeId: { userId: 1, postId: 1 } },
  data: { postId: 2 },
});

findUnique、findUniqueOrThrow、update、delete、upsert 都接受复合 ID,关系写入的 connect / connectOrCreate 里同样能用:

await prisma.user.create({
  data: {
    name: "Alice",
    likes: {
      connect: { likeId: { postId: 1, userId: 2 } },
    },
  },
});

MongoDB 不支持复合 ID,也一并留 v6.19。

参考来源

  • Prisma 官方文档:Working with Json fields
  • Prisma 官方文档:Working with scalar lists / arrays
  • Prisma 官方文档:Null and undefined
  • Prisma 官方文档:Working with composite IDs and constraints
  • Prisma 官方文档:Fields & types(Decimal / BigInt / Bytes)
  • Mapagam:Working with JSON Fields / Handling Optional and Null Values