跳到主要内容

使用 Json 字段

使用 Json Prisma ORM 字段类型来读取、写入以及对底层数据库中的 JSON 类型进行基本过滤。在下面的示例中,User 模型有一个名为 extendedPetsData 的可选 Json 字段

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
extendedPetsData Json?
}

示例字段值

{
"pet1": {
"petName": "Claudine",
"petType": "House cat"
},
"pet2": {
"petName": "Sunny",
"petType": "Gerbil"
}
}

Json 字段支持一些额外的类型,例如 stringboolean。这些额外的类型是为了匹配 JSON.parse() 支持的类型而存在的

export type JsonValue =
| string
| number
| boolean
| null
| JsonObject
| JsonArray

JSON 字段的使用场景

将数据存储为 JSON 而非表示为关联模型的理由包括

  • 你需要存储结构不一致的数据
  • 你正在从另一个系统导入数据,并且不想将该数据映射到 Prisma 模型

读取 Json 字段

你可以使用 Prisma.JsonArrayPrisma.JsonObject 工具类来处理 Json 字段的内容

const { PrismaClient, Prisma } = require('@prisma/client')

const user = await prisma.user.findFirst({
where: {
id: 9,
},
})

// Example extendedPetsData data:
// [{ name: 'Bob the dog' }, { name: 'Claudine the cat' }]

if (
user?.extendedPetsData &&
typeof user?.extendedPetsData === 'object' &&
Array.isArray(user?.extendedPetsData)
) {
const petsObject = user?.extendedPetsData as Prisma.JsonArray

const firstPet = petsObject[0]
}

另请参阅:高级示例:更新嵌套 JSON 键值

写入 Json 字段

以下示例将一个 JSON 对象写入 extendedPetsData 字段

var json = [
{ name: 'Bob the dog' },
{ name: 'Claudine the cat' },
] as Prisma.JsonArray

const createUser = await prisma.user.create({
data: {
email: 'birgitte@prisma.io',
extendedPetsData: json,
},
})

注意:JavaScript 对象(例如 { extendedPetsData: "none"})会自动转换为 JSON。

另请参阅:高级示例:更新嵌套 JSON 键值

过滤 Json 字段 (简单)

你可以过滤 Json 类型的行。

按精确字段值过滤

以下查询返回所有 extendedPetsData 的值与 json 变量完全匹配的用户

var json = { [{ name: 'Bob the dog' }, { name: 'Claudine the cat' }] }

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
equals: json,
},
},
})

以下查询返回所有 extendedPetsData 的值与 json 变量完全匹配的用户

var json = {
extendedPetsData: [{ name: 'Bob the dog' }, { name: 'Claudine the cat' }],
}

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
not: json,
},
},
})

过滤 Json 字段 (高级)

你还可以根据 Json 字段内的数据进行过滤。我们称之为高级 Json 过滤。此功能仅由 PostgreSQLMySQL 支持,path 选项的语法不同

警告

PostgreSQL 不支持在数组中过滤对象键值

信息

高级 Json 过滤的可用性取决于你的 Prisma 版本

根据数据库不同的 path 语法

下面的过滤器使用 path 选项来选择 Json 值中的特定部分进行过滤。不同连接器实现该过滤的方式不同

例如,以下是一个有效的 MySQL path

$petFeatures.petName

以下是一个有效的 PostgreSQL path

["petFeatures", "petName"]

按对象属性过滤

你可以过滤 JSON 块中的特定属性。在以下示例中,extendedPetsData 的值是一个一维、非嵌套的 JSON 对象

{
"petName": "Claudine",
"petType": "House cat"
}

以下查询返回所有 petName 的值为 "Claudine" 的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['petName'],
equals: 'Claudine',
},
},
})

以下查询返回所有 petType 的值包含 "cat" 的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['petType'],
string_contains: 'cat',
},
},
})

以下字符串过滤器可用

要使用这些过滤器的不区分大小写版本,你可以使用 mode 选项

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['petType'],
string_contains: 'cat',
mode: 'insensitive'
},
},
})

按嵌套对象属性过滤

你可以过滤嵌套的 JSON 属性。在以下示例中,extendedPetsData 的值是一个具有多层嵌套的 JSON 对象。

{
"pet1": {
"petName": "Claudine",
"petType": "House cat"
},
"pet2": {
"petName": "Sunny",
"petType": "Gerbil",
"features": {
"eyeColor": "Brown",
"furColor": "White and black"
}
}
}

以下查询返回所有 "pet2""petName" 的值为 "Sunny" 的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['pet2', 'petName'],
equals: 'Sunny',
},
},
})

以下查询返回所有满足以下条件的用户:

  • "pet2""petName" 的值为 "Sunny"
  • "pet2""features""furColor" 包含 "black"
const getUsers = await prisma.user.findMany({
where: {
AND: [
{
extendedPetsData: {
path: ['pet2', 'petName'],
equals: 'Sunny',
},
},
{
extendedPetsData: {
path: ['pet2', 'features', 'furColor'],
string_contains: 'black',
},
},
],
},
})

按数组值过滤

你可以过滤标量数组(字符串、整数)中是否存在特定值。在以下示例中,extendedPetsData 的值是一个字符串数组

["Claudine", "Sunny"]

以下查询返回所有拥有一只名为 "Claudine" 的宠物的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
array_contains: ['Claudine'],
},
},
})
信息

注意:在 PostgreSQL 中,array_contains 的值必须是数组而不是字符串,即使该数组只包含一个值。

以下数组过滤器可用

按嵌套数组值过滤

你可以过滤标量数组(字符串、整数)中是否存在特定值。在以下示例中,extendedPetsData 的值包含嵌套的标量名称数组

{
"cats": { "owned": ["Bob", "Sunny"], "fostering": ["Fido"] },
"dogs": { "owned": ["Ella"], "fostering": ["Prince", "Empress"] }
}

标量值数组

以下查询返回所有寄养一只名为 "Fido" 的猫的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['cats', 'fostering'],
array_contains: ['Fido'],
},
},
})
信息

注意:在 PostgreSQL 中,array_contains 的值必须是数组而不是字符串,即使该数组只包含一个值。

以下查询返回所有寄养名为 "Fido" "Bob" 的猫的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['cats', 'fostering'],
array_contains: ['Fido', 'Bob'],
},
},
})

JSON 对象数组

const json = [{ status: 'expired', insuranceID: 92 }]

const checkJson = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['insurances'],
array_contains: json,
},
},
})
  • 如果你使用 PostgreSQL,你必须传入一个对象数组进行匹配,即使该数组只包含一个对象

    [{ status: 'expired', insuranceID: 92 }]
    // PostgreSQL

    如果你使用 MySQL,你必须传入单个对象进行匹配

    { status: 'expired', insuranceID: 92 }
    // MySQL
  • 如果你的过滤数组包含多个对象,PostgreSQL 只会在所有对象都存在时返回结果 - 而不是至少一个对象存在时。

  • 你必须将 array_contains 设置为一个 JSON 对象,而不是字符串。如果你使用字符串,Prisma Client 会转义引号,并且查询不会返回结果。例如

    array_contains: '[{"status": "expired", "insuranceID": 92}]'

    会作为以下内容发送到数据库:

    [{\"status\": \"expired\", \"insuranceID\": 92}]

按索引定位数组元素

你可以过滤位于特定位置的元素的值。

{ "owned": ["Bob", "Sunny"], "fostering": ["Fido"] }
const getUsers = await prisma.user.findMany({
where: {
comments: {
path: ['owned', '1'],
string_contains: 'Bob',
},
},
})

过滤数组中的对象键值

根据你的提供者,你可以过滤数组中对象的键值。

警告

过滤数组中的对象键值MySQL 数据库连接器支持。但是,你仍然可以过滤是否存在完整的 JSON 对象

在以下示例中,extendedPetsData 的值是一个对象数组,其中包含一个嵌套的 insurances 数组,该数组包含两个对象

[
{
"petName": "Claudine",
"petType": "House cat",
"insurances": [
{ "insuranceID": 92, "status": "expired" },
{ "insuranceID": 12, "status": "active" }
]
},
{
"petName": "Sunny",
"petType": "Gerbil"
},
{
"petName": "Gerald",
"petType": "Corn snake"
},
{
"petName": "Nanna",
"petType": "Moose"
}
]

以下查询返回所有至少有一只宠物是驼鹿的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: '$[*].petType',
array_contains: 'Moose',
},
},
})
  • $[*] 是宠物对象的根数组
  • petType 匹配任何宠物对象中的 petType

以下查询返回所有至少有一只宠物有已过期保险的用户

const getUsers = await prisma.user.findMany({
where: {
extendedPetsData: {
path: '$[*].insurances[*].status',
array_contains: 'expired',
},
},
})
  • $[*] 是宠物对象的根数组
  • insurances[*] 匹配任何宠物对象内的任何 insurances 数组
  • status 匹配任何保险对象内的任何 status

高级示例:更新嵌套 JSON 键值

以下示例假设 extendedPetsData 的值是以下某种变体

{
"petName": "Claudine",
"petType": "House cat",
"insurances": [
{ "insuranceID": 92, "status": "expired" },
{ "insuranceID": 12, "status": "active" }
]
}

以下示例:

  1. 获取所有用户
  2. 将每个保险对象的 "status" 更改为 "expired"
  3. 获取所有 ID 为 92 且有已过期保险的用户
const userQueries: string | any[] = []

getUsers.forEach((user) => {
if (
user.extendedPetsData &&
typeof user.extendedPetsData === 'object' &&
!Array.isArray(user.extendedPetsData)
) {
const petsObject = user.extendedPetsData as Prisma.JsonObject

const i = petsObject['insurances']

if (i && typeof i === 'object' && Array.isArray(i)) {
const insurancesArray = i as Prisma.JsonArray

insurancesArray.forEach((i) => {
if (i && typeof i === 'object' && !Array.isArray(i)) {
const insuranceObject = i as Prisma.JsonObject

insuranceObject['status'] = 'expired'
}
})

const whereClause = Prisma.validator<Prisma.UserWhereInput>()({
id: user.id,
})

const dataClause = Prisma.validator<Prisma.UserUpdateInput>()({
extendedPetsData: petsObject,
})

userQueries.push(
prisma.user.update({
where: whereClause,
data: dataClause,
})
)
}
}
})

if (userQueries.length > 0) {
console.log(userQueries.length + ' queries to run!')
await prisma.$transaction(userQueries)
}

const json = [{ status: 'expired', insuranceID: 92 }]

const checkJson = await prisma.user.findMany({
where: {
extendedPetsData: {
path: ['insurances'],
array_contains: json,
},
},
})

console.log(checkJson.length)

使用 null

SQL 数据库中的 JSON 字段有两种可能的 null 值类型。

  • 数据库 NULL:数据库中的值是 NULL
  • JSON null:数据库中的值包含一个 JSON 值,该值为 null

为了区分这些可能性,我们引入了三种你可以使用的null 枚举

  • JsonNull:表示 JSON 中的 null 值。
  • DbNull:表示数据库中的 NULL 值。
  • AnyNull:表示 null JSON 值和 NULL 数据库值。(仅用于过滤时)
信息

从 v4.0.0 起,JsonNullDbNullAnyNull 是对象。在 v4.0.0 之前,它们是字符串。

信息
  • 使用任何null 枚举进行过滤时,你不能使用简写形式而省略 equals 操作符。
  • 这些null 枚举不适用于 MongoDB,因为在 MongoDB 中 JSON null 和数据库 NULL 没有区别。
  • 这些null 枚举不适用于所有数据库中的 array_contains 操作符,因为 JSON 数组中只能包含 JSON null。由于 JSON 数组中不能包含数据库 NULL,因此 { array_contains: null } 没有歧义。

例如

model Log {
id Int @id
meta Json
}

这是一个使用 AnyNull 的示例

import { Prisma } from '@prisma/client'

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

插入 null

这也适用于 createupdateupsert。要将 JSON null 值插入 Json 字段,你可以这样写:

import { Prisma } from '@prisma/client'

prisma.log.create({
data: {
meta: Prisma.JsonNull,
},
})

要将数据库 NULL 插入 Json 字段,你可以这样写:

import { Prisma } from '@prisma/client'

prisma.log.create({
data: {
meta: Prisma.DbNull,
},
})

null 值过滤

要按 JsonNullDbNull 过滤,你可以这样写:

import { Prisma } from '@prisma/client'

prisma.log.findMany({
where: {
meta: {
equals: Prisma.AnyNull,
},
},
})
信息

这些null 枚举不适用于 MongoDB,因为 MongoDB 不区分 JSON null 和数据库 NULL。它们也不适用于所有数据库中的 array_contains 操作符,因为 JSON 数组中只能包含 JSON null。由于 JSON 数组中不能包含数据库 NULL,因此 { array_contains: null } 没有歧义。

类型化的 Json

默认情况下,Json 字段在 Prisma 模型中没有类型化。要在这些字段内实现强类型化,你需要使用像 prisma-json-types-generator 这样的外部包来完成此操作。

使用 prisma-json-types-generator

首先,根据包的说明安装并配置 prisma-json-types-generator

然后,假设你有一个如下所示的模型

model Log {
id Int @id
meta Json
}

你可以使用抽象语法树注释更新并对其进行类型化

schema.prisma
model Log {
id Int @id

/// [LogMetaType]
meta Json
}

然后,确保你在包含在 tsconfig.json 中的类型声明文件中定义了上述类型

types.ts
declare global {
namespace PrismaJson {
type LogMetaType = { timestamp: number; host: string }
}
}

现在,当你处理 Log.meta 时,它将是强类型化的!

Json 常见问题

你可以选择要返回的 JSON 键/值子集吗?

不行 - 尚未实现选择要返回哪些 JSON 元素的功能。Prisma Client 返回整个 JSON 对象。

你可以过滤是否存在特定的键吗?

不行 - 尚未实现过滤是否存在特定键的功能。

支持不区分大小写的过滤吗?

不行 - 尚不支持不区分大小写的过滤

你可以按 JSON 值中的对象属性排序吗?

不行,目前不支持按 JSON 值中的对象属性排序 (order-by-prop)

如何为 JSON 字段设置默认值?

当你想为 Json 类型设置 @default 值时,你需要将它用双引号括起来放在 @default 属性内部(并且可能需要使用反斜杠转义任何“内部”的双引号),例如

model User {
id Int @id @default(autoincrement())
json1 Json @default("[]")
json2 Json @default("{ \"hello\": \"world\" }")
}