复合类型
复合类型仅在使用 MongoDB 时可用。
复合类型,在 MongoDB 中称为 嵌入式文档,允许您将记录嵌入到其他记录中。
我们在 v3.12.0 版本中将复合类型设为 正式可用 (GA)。此前,它们从 v3.10.0 版本开始在 预览 版中可用。
本页解释了如何
- 使用
findFirst
和findMany
查找 包含复合类型的记录 - 使用
create
和createMany
创建 包含复合类型的新记录 - 使用
update
和updateMany
更新 现有记录中的复合类型 - 使用
delete
和deleteMany
删除 包含复合类型的记录
示例 Schema
我们将使用此 schema 进行以下示例
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 中使用复合类型时存在一些限制
findUnique()
不能按复合类型过滤aggregate
、groupBy()
、count
不支持复合操作
复合类型中必需字段的默认值
从 4.0.0 版本开始,如果您在满足以下所有条件时对复合类型进行数据库读取,Prisma Client 会将默认值插入到结果中。
条件
注意
- 这与模型字段的行为相同。
- 在读取操作中,Prisma Client 将默认值插入到结果中,但不会将默认值插入到数据库中。
在我们的示例 schema 中,假设您向 photo
添加了一个必需字段。此字段 bitDepth
有一个默认值
...
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".
使用 find
和 findMany
查找包含复合类型的记录
可以在 where
操作中按复合类型过滤记录。
以下部分描述了按单一类型或多种类型进行过滤的可用操作,并提供了每个操作的示例。
过滤单一复合类型
使用 is
、equals
、isNot
和 isSet
操作来更改单一复合类型
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,
},
},
})
过滤多个复合类型
使用 equals
、isEmpty
、every
、some
和 none
操作来过滤多个复合类型
equals
: 检查列表是否完全相等isEmpty
: 检查列表是否为空every
: 列表中所有项都必须匹配条件some
: 列表中一项或多项必须匹配条件none
: 列表中没有项可以匹配条件isSet
: 过滤可选字段,仅包含已设置的结果(设置为某个值,或显式设置为null
)。将此过滤器设置为true
将排除根本未设置的undefined
结果。
例如,您可以使用 equals
查找具有特定照片列表的产品(所有 url
、height
和 width
字段都必须匹配)
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',
},
},
},
})
使用 create
和 createMany
创建包含复合类型的记录
当您创建一个包含具有唯一约束的复合类型的记录时,请注意 MongoDB 不强制执行记录内部的唯一值。了解更多。
可以使用 set
操作在 create
或 createMany
方法中创建复合类型。例如,您可以使用 create
中的 set
在 Order
内部创建一个 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,
},
},
})
为了模拟 product
包含多个 photos
列表的情况,您可以一次 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
,每个产品都包含一个 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' },
],
},
],
})
在 update
和 updateMany
中更改复合类型
当您更新一个包含具有唯一约束的复合类型的记录时,请注意 MongoDB 不强制执行记录内部的唯一值。了解更多。
可以在 update
或 updateMany
方法中设置、更新或删除复合类型。以下部分描述了同时更新单一类型或多种类型的可用操作,并提供了每个操作的示例。
更改单一复合类型
使用 set
、unset
、update
和 upsert
操作来更改单一复合类型
- 使用
set
设置一个复合类型,覆盖任何现有值 - 使用
unset
来取消设置一个复合类型。与set: null
不同,unset
会完全移除该字段 - 使用
update
更新一个复合类型 - 使用
upsert
来update
现有复合类型(如果存在),否则set
该复合类型
例如,使用 update
更新 Order
内部必需的 shippingAddress
(一个 Address
复合类型)
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
操作移除可选的嵌入式类型。以下示例使用 unset
从 Order
中移除 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',
},
},
},
})
更改多个复合类型
使用 set
、push
、updateMany
和 deleteMany
操作来更改复合类型的列表
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
更新 url
为 1.jpg
或 2.png
的照片
const product = prisma.product.update({
where: {
id: '62de6d328a65d8fffdae2c18',
},
data: {
photos: {
updateMany: {
where: {
url: '1.jpg',
},
data: {
url: '2.png',
},
},
},
},
})
以下示例使用 deleteMany
删除所有 height
为 100 的照片
const product = prisma.product.update({
where: {
id: '62de6d328a65d8fffdae2c18',
},
data: {
photos: {
deleteMany: {
where: {
height: 100,
},
},
},
},
})
使用 upsert
进行复合类型的 upsert 操作
当您创建或更新包含具有唯一约束的复合类型中的值时,请注意 MongoDB 不强制执行记录内部的唯一值。了解更多。
要创建或更新复合类型,请使用 upsert
方法。您可以使用与上面 create
和 update
方法相同的复合操作。
例如,使用 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' },
},
},
})
使用 delete
和 deleteMany
删除包含复合类型的记录
要移除嵌入了复合类型的记录,请使用 delete
或 deleteMany
方法。这也会移除嵌入的复合类型。
例如,使用 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 不会抛出错误,并且会在 addresses
中存储两次 alice@prisma.io
。
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 强制执行记录中的唯一值。Mailbox
和 Address
模型之间存在关系。此外,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
的地址。