跳到主要内容

复合类型

警告

复合类型仅适用于 MongoDB。

复合类型,在 MongoDB 中称为嵌入式文档,允许你将记录嵌入到其他记录中。

我们在 v3.12.0 版本中将复合类型正式发布 (GA)。此前,它们在 v3.10.0 版本中处于预览阶段。

本页解释如何

  • 使用 findFirstfindMany 查找包含复合类型的记录
  • 使用 createcreateMany 创建带有复合类型的新记录
  • 使用 updateupdateMany 更新现有记录中的复合类型
  • 使用 deletedeleteMany 删除带有复合类型的记录

示例 schema

我们将使用以下 schema 作为示例

schema.prisma
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

model Product {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String @unique
price Float
colors Color[]
sizes Size[]
photos Photo[]
orders Order[]
}

model Order {
id String @id @default(auto()) @map("_id") @db.ObjectId
product Product @relation(fields: [productId], references: [id])
color Color
size Size
shippingAddress Address
billingAddress Address?
productId String @db.ObjectId
}

enum Color {
Red
Green
Blue
}

enum Size {
Small
Medium
Large
XLarge
}

type Photo {
height Int @default(200)
width Int @default(100)
url String
}

type Address {
street String
city String
zip String
}

在此 schema 中,Product 模型具有一个 Photo[] 复合类型,而 Order 模型具有两个复合 Address 类型。shippingAddress 是必填项,但 billingAddress 是可选项。

使用复合类型时的注意事项

目前在 Prisma Client 中使用复合类型时存在一些限制

复合类型上必填字段的默认值

从 4.0.0 版本开始,如果你在满足以下所有条件时对复合类型执行数据库读取操作,Prisma Client 会将默认值插入到结果中。

条件

  • 复合类型上的某个字段是必填项,并且
  • 该字段具有默认值,并且
  • 该字段在返回的文档中不存在。

注意

  • 这与模型字段的行为相同。
  • 在读取操作中,Prisma Client 将默认值插入到结果中,但不会将默认值插入到数据库中。

在我们的示例 schema 中,假设你为 photo 添加了一个必填字段。此字段 bitDepth 具有默认值

schema.prisma
...
type Photo {
...
bitDepth Int @default(8)
}

...

假设你接着运行 npx prisma db push更新数据库,并使用 npx prisma generate 重新生成 Prisma Client。然后,你运行以下应用程序代码

console.dir(await prisma.product.findMany({}), { depth: Infinity })

bitDepth 字段没有内容,因为你刚刚添加此字段,因此查询返回默认值 8

** 早期版本 **

在 4.0.0 版本之前,Prisma ORM 会抛出如下 P2032 错误

Error converting field "bitDepth" of expected non-nullable
type "int", found incompatible value of "null".

使用 findfindMany 查找包含复合类型的记录

记录可以通过 where 操作中的复合类型进行筛选。

以下部分描述了用于按单一类型或多种类型进行筛选的可用操作,并提供了每个操作的示例。

筛选单个复合类型

使用 isequalsisNotisSet 操作来更改单个复合类型

  • is:通过匹配复合类型来筛选结果。要求存在一个或多个字段 (例如,按送货地址中的街道名称筛选订单)
  • equals:通过匹配复合类型来筛选结果。要求所有字段都存在。 (例如,按完整的送货地址筛选订单)
  • isNot:通过不匹配的复合类型来筛选结果
  • isSet:筛选可选字段以仅包含已设置(设置为某个值或明确设置为 null)的结果。将此过滤器设置为 true 将排除根本未设置的 undefined 结果。

例如,使用 is 筛选街道名称为 '555 Candy Cane Lane' 的订单

const orders = await prisma.order.findMany({
where: {
shippingAddress: {
is: {
street: '555 Candy Cane Lane',
},
},
},
})

使用 equals 筛选送货地址中所有字段都匹配的订单

const orders = await prisma.order.findMany({
where: {
shippingAddress: {
equals: {
street: '555 Candy Cane Lane',
city: 'Wonderland',
zip: '52337',
},
},
},
})

你也可以使用此查询的简写形式,省略 equals

const orders = await prisma.order.findMany({
where: {
shippingAddress: {
street: '555 Candy Cane Lane',
city: 'Wonderland',
zip: '52337',
},
},
})

使用 isNot 筛选邮政编码不是 '52337' 的订单

const orders = await prisma.order.findMany({
where: {
shippingAddress: {
isNot: {
zip: '52337',
},
},
},
})

使用 isSet 筛选可选的 billingAddress 已设置(设置为某个值或 null)的订单

const orders = await prisma.order.findMany({
where: {
billingAddress: {
isSet: true,
},
},
})

筛选多个复合类型

使用 equalsisEmptyeverysomenone 操作来筛选多个复合类型

  • equals:检查列表的精确相等性
  • isEmpty:检查列表是否为空
  • every:列表中的每个项目都必须匹配条件
  • some:列表中一个或多个项目必须匹配条件
  • none:列表中没有任何项目可以匹配条件
  • isSet:筛选可选字段以仅包含已设置(设置为某个值或明确设置为 null)的结果。将此过滤器设置为 true 将排除根本未设置的 undefined 结果。

例如,你可以使用 equals 查找具有特定照片列表的产品(所有 urlheightwidth 字段必须匹配)

const product = prisma.product.findMany({
where: {
photos: {
equals: [
{
url: '1.jpg',
height: 200,
width: 100,
},
{
url: '2.jpg',
height: 200,
width: 100,
},
],
},
},
})

你也可以使用此查询的简写形式,省略 equals,只指定要筛选的字段

const product = prisma.product.findMany({
where: {
photos: [
{
url: '1.jpg',
height: 200,
width: 100,
},
{
url: '2.jpg',
height: 200,
width: 100,
},
],
},
})

使用 isEmpty 筛选没有照片的产品

const product = prisma.product.findMany({
where: {
photos: {
isEmpty: true,
},
},
})

使用 some 筛选其中一个或多个照片的 url"2.jpg" 的产品

const product = prisma.product.findFirst({
where: {
photos: {
some: {
url: '2.jpg',
},
},
},
})

使用 none 筛选没有照片的 url"2.jpg" 的产品

const product = prisma.product.findFirst({
where: {
photos: {
none: {
url: '2.jpg',
},
},
},
})

使用 createcreateMany 创建带有复合类型的记录

信息

当你创建带有唯一约束的复合类型记录时,请注意 MongoDB 不会在记录内部强制执行唯一值。了解更多

复合类型可以使用 set 操作在 createcreateMany 方法中创建。例如,你可以在 create 中使用 setOrder 内部创建 Address 复合类型

const order = await prisma.order.create({
data: {
// Normal relation
product: { connect: { id: 'some-object-id' } },
color: 'Red',
size: 'Large',
// Composite type
shippingAddress: {
set: {
street: '1084 Candycane Lane',
city: 'Silverlake',
zip: '84323',
},
},
},
})

你也可以使用简写形式,省略 set,只指定要创建的字段

const order = await prisma.order.create({
data: {
// Normal relation
product: { connect: { id: 'some-object-id' } },
color: 'Red',
size: 'Large',
// Composite type
shippingAddress: {
street: '1084 Candycane Lane',
city: 'Silverlake',
zip: '84323',
},
},
})

对于可选类型,如 billingAddress,你也可以将值设置为 null

const order = await prisma.order.create({
data: {
// Normal relation
product: { connect: { id: 'some-object-id' } },
color: 'Red',
size: 'Large',
// Composite type
shippingAddress: {
street: '1084 Candycane Lane',
city: 'Silverlake',
zip: '84323',
},
// Embedded optional type, set to null
billingAddress: {
set: null,
},
},
})

要为包含多个 photos 列表的 product 建模,你可以同时 set 多个复合类型

const product = await prisma.product.create({
data: {
name: 'Forest Runners',
price: 59.99,
colors: ['Red', 'Green'],
sizes: ['Small', 'Medium', 'Large'],
// New composite type
photos: {
set: [
{ height: 100, width: 200, url: '1.jpg' },
{ height: 100, width: 200, url: '2.jpg' },
],
},
},
})

你也可以使用简写形式,省略 set,只指定要创建的字段

const product = await prisma.product.create({
data: {
name: 'Forest Runners',
price: 59.99,
// Scalar lists that we already support
colors: ['Red', 'Green'],
sizes: ['Small', 'Medium', 'Large'],
// New composite type
photos: [
{ height: 100, width: 200, url: '1.jpg' },
{ height: 100, width: 200, url: '2.jpg' },
],
},
})

这些操作也适用于 createMany 方法。例如,你可以创建多个 product,每个 product 都包含一个 photos 列表

const product = await prisma.product.createMany({
data: [
{
name: 'Forest Runners',
price: 59.99,
colors: ['Red', 'Green'],
sizes: ['Small', 'Medium', 'Large'],
photos: [
{ height: 100, width: 200, url: '1.jpg' },
{ height: 100, width: 200, url: '2.jpg' },
],
},
{
name: 'Alpine Blazers',
price: 85.99,
colors: ['Blue', 'Red'],
sizes: ['Large', 'XLarge'],
photos: [
{ height: 100, width: 200, url: '1.jpg' },
{ height: 150, width: 200, url: '4.jpg' },
{ height: 200, width: 200, url: '5.jpg' },
],
},
],
})

updateupdateMany 中更改复合类型

信息

当你更新带有唯一约束的复合类型记录时,请注意 MongoDB 不会在记录内部强制执行唯一值。了解更多

复合类型可以在 updateupdateMany 方法中设置、更新或删除。以下部分描述了用于一次更新单个类型或多个类型的可用操作,并提供了每个操作的示例。

更改单个复合类型

使用 setunsetupdateupsert 操作来更改单个复合类型

  • 使用 set 设置复合类型,覆盖任何现有值
  • 使用 unset 取消设置复合类型。与 set: null 不同,unset 会完全删除该字段
  • 使用 update 更新复合类型
  • 使用 upsert 如果存在则 update 现有复合类型,否则 set 复合类型

例如,使用 update 更新 Order 中具有 Address 复合类型的必填 shippingAddress

const order = await prisma.order.update({
where: {
id: 'some-object-id',
},
data: {
shippingAddress: {
// Update just the zip field
update: {
zip: '41232',
},
},
},
})

对于可选的嵌入式类型,如 billingAddress,使用 upsert 如果记录不存在则创建新记录,如果存在则更新记录

const order = await prisma.order.update({
where: {
id: 'some-object-id',
},
data: {
billingAddress: {
// Create the address if it doesn't exist,
// otherwise update it
upsert: {
set: {
street: '1084 Candycane Lane',
city: 'Silverlake',
zip: '84323',
},
update: {
zip: '84323',
},
},
},
},
})

你也可以使用 unset 操作来删除可选的嵌入式类型。以下示例使用 unsetOrder 中删除 billingAddress

const order = await prisma.order.update({
where: {
id: 'some-object-id',
},
data: {
billingAddress: {
// Unset the billing address
// Removes "billingAddress" field from order
unset: true,
},
},
})

你可以在 updateMany 中使用过滤器来更新所有匹配复合类型的记录。以下示例使用 is 过滤器匹配订单列表中送货地址的街道名称

const orders = await prisma.order.updateMany({
where: {
shippingAddress: {
is: {
street: '555 Candy Cane Lane',
},
},
},
data: {
shippingAddress: {
update: {
street: '111 Candy Cane Drive',
},
},
},
})

更改多个复合类型

使用 setpushupdateManydeleteMany 操作来更改复合类型列表

  • set:设置嵌入式复合类型列表,覆盖任何现有列表
  • push:将值推送到嵌入式复合类型列表的末尾
  • updateMany:一次更新多个复合类型
  • deleteMany:一次删除多个复合类型

例如,使用 push 将新照片添加到 photos 列表

const product = prisma.product.update({
where: {
id: '62de6d328a65d8fffdae2c18',
},
data: {
photos: {
// Push a photo to the end of the photos list
push: [{ height: 100, width: 200, url: '1.jpg' }],
},
},
})

使用 updateMany 更新 url1.jpg2.png 的照片

const product = prisma.product.update({
where: {
id: '62de6d328a65d8fffdae2c18',
},
data: {
photos: {
updateMany: {
where: {
url: '1.jpg',
},
data: {
url: '2.png',
},
},
},
},
})

以下示例使用 deleteMany 删除所有高度为 100 的照片

const product = prisma.product.update({
where: {
id: '62de6d328a65d8fffdae2c18',
},
data: {
photos: {
deleteMany: {
where: {
height: 100,
},
},
},
},
})

使用 upsert 插入复合类型

信息

当你创建或更新具有唯一约束的复合类型中的值时,请注意 MongoDB 不会在记录内部强制执行唯一值。了解更多

要创建或更新复合类型,请使用 upsert 方法。你可以使用与上述 createupdate 方法相同的复合操作。

例如,使用 upsert 来创建新产品或向现有产品添加照片

const product = await prisma.product.upsert({
where: {
name: 'Forest Runners',
},
create: {
name: 'Forest Runners',
price: 59.99,
colors: ['Red', 'Green'],
sizes: ['Small', 'Medium', 'Large'],
photos: [
{ height: 100, width: 200, url: '1.jpg' },
{ height: 100, width: 200, url: '2.jpg' },
],
},
update: {
photos: {
push: { height: 300, width: 400, url: '3.jpg' },
},
},
})

使用 deletedeleteMany 删除包含复合类型的记录

要删除嵌入复合类型的记录,请使用 deletedeleteMany 方法。这也会删除嵌入的复合类型。

例如,使用 deleteMany 删除所有 size"Small" 的产品。这也会删除任何嵌入的 photos

const deleteProduct = await prisma.product.deleteMany({
where: {
sizes: {
equals: 'Small',
},
},
})

你还可以使用过滤器来删除匹配复合类型的记录。以下示例使用 some 过滤器删除包含特定照片的产品

const product = await prisma.product.deleteMany({
where: {
photos: {
some: {
url: '2.jpg',
},
},
},
})

复合类型排序

你可以使用 orderBy 操作以升序或降序对结果进行排序。

例如,以下命令查找所有订单并按送货地址中的城市名称以升序排序

const orders = await prisma.order.findMany({
orderBy: {
shippingAddress: {
city: 'asc',
},
},
})

复合类型唯一字段中的重复值

当你对具有唯一约束的复合类型记录执行任何以下操作时,请务必小心。在这种情况下,MongoDB 不会在记录内部强制执行唯一值。

  • 当你创建记录时
  • 当你向记录添加数据时
  • 当你更新记录中的数据时

如果你的 schema 有一个带有 @@unique 约束的复合类型,MongoDB 会阻止你在包含此复合类型的两个或多个记录中为约束值存储相同的值。然而,MongoDB 并不会阻止你在单个记录中存储同一字段值的多个副本。

请注意,你可以使用 Prisma ORM 关系来解决此问题

例如,在以下 schema 中,MailBox 有一个复合类型 addresses,它在 email 字段上有一个 @@unique 约束。

type Address {
email String
}

model MailBox {
name String
addresses Address[]

@@unique([addresses.email])
}

以下代码创建了一个记录,其中 address 中有两个相同的值。MongoDB 在这种情况下不会抛出错误,它会将 alice@prisma.io 存储在 addresses 中两次。

await prisma.MailBox.createMany({
data: [
{
name: 'Alice',
addresses: {
set: [
{
address: 'alice@prisma.io', // Not unique
},
{
address: 'alice@prisma.io', // Not unique
},
],
},
},
],
})

注意:如果你尝试在两个独立的记录中存储相同的值,MongoDB 会抛出错误。在上面的示例中,如果你尝试为用户 Alice 和用户 Bob 存储相同的电子邮件地址 alice@prisma.io,MongoDB 将不会存储数据并抛出错误。

使用 Prisma ORM 关系强制执行记录中的唯一值

在上面的示例中,MongoDB 没有强制执行嵌套地址名称上的唯一约束。但是,你可以通过不同方式建模数据来强制执行记录中的唯一值。为此,请使用 Prisma ORM 关系将复合类型转换为集合。设置与此集合的关系,并在要唯一的字段上放置唯一约束。

在以下示例中,MongoDB 会强制执行记录中的唯一值。MailboxAddress 模型之间存在关系。此外,Address 模型中的 name 字段具有唯一约束。

model Address {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
mailbox Mailbox? @relation(fields: [mailboxId], references: [id])
mailboxId String? @db.ObjectId

@@unique([name])
}

model Mailbox {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
addresses Address[] @relation
}
await prisma.MailBox.create({
data: {
name: 'Alice',
addresses: {
create: [
{ name: 'alice@prisma.io' }, // Not unique
{ name: 'alice@prisma.io' }, // Not unique
],
},
},
})

如果你运行上述代码,MongoDB 将强制执行唯一约束。它不允许你的应用程序添加两个名称为 alice@prisma.io 的地址。

© . All rights reserved.