前言
本文翻译自 Prisma 官方文档,MongoDB 使用部分,因为Prisma 从4.0之后才比较正式的加入 MongoDB 的支持,相关中文文档和资料也没有,本人也一直关注 Prisma 和 MongoDB 的动向,所以才着手翻译。鉴于 MongoDB 这种文档型数据库的特殊性,兼容问题一直不太放心,最近看到官方的支持越来越完善,希望早日能投入生产。时间精力有限,错漏之处还望指正,如对你有所帮助还请不吝点赞支持一波~后续还会持续更新相关文档的翻译。
本指南讨论了使用Prisma和MongoDB背后的概念,解释了MongoDB和其他数据库提供者的共同点和不同点,并引导你完成在应用程序中配置Prisma与MongoDB集成的过程。
MongoDB是什么?
MongoDB是一个NoSQL数据库,它将数据存储为BSON格式,这是一种类似JSON的文档格式,旨在将数据存储在键值对种。它通常被用在 JavaScript 应用程序的开发中,因为在应用程序代码中,这种文档模型非常容易被映射为对象,并且原生就支持高可用性和水平扩展。
MongoDB将数据存储在不需要预先定义模式的集合中,正如你需要处理关系数据库中的表一样。每个集合的结构可以随着时间的推移而发生改变。这种灵活性可以让你的数据模型快速迭代,但这确实意味着在使用Prisma处理MongoDB数据库时存在许多差异。
与其他数据库提供者的共同点
将Prisma和MongoDB一起使用的某些方面与Prisma和关系型数据库一期使用时相同。你仍可以:
- 使用Prisma Schema Language 进行数据库建模
- 使用
mongodb
database connector 连接你的数据库 - 如果你已经有一个MongoDB数据库,你可以对已存在的项目使用内省
- 使用
db push
命令把 schema中的变更推送到你的数据库 - 在你的应用里使用Prisma客户端根据你的Prisma Schema 以类型安全的方式来查询你的数据库
需要关注的差异
MongoDB的基于文档的结构和灵活的模式意味着将Prisma和MongoDB一起使用与将其与关系型数据库一起使用在许多方面有所不同。以下是你需要注意的一些差异:
- 定义ID:MongoDB文档有一个
_id
字段(通常是一个ObjectID)。Prisma不支持以_
开头的字段,因此需要用@map
属性来映射一个Prisma字段。要了解更多信息,可查阅MongoDB中定义ID。 - 迁移现有数据以匹配你的Prisma schema:在关系型数据库中,所有数据都必需匹配你的schema。当你迁移时如果你修改了schema中一个特定字段的类型,所有数据也必须更新以匹配。相比之下,MongoDB不强制更新任何特定 schema,所以当你迁移时需要特别小心。要了解更多信息,可查阅 如何迁移旧数据到新的 schemas 。
- 内省与 Prisma 关系:当你内省一个已存在的MongoDB数据库时,你将得到一个没有关联关系的schema,而且需要将缺失的关系手动添加回去。要了解更多信息,可查阅在内省之后如何添加缺失的关系。
- 筛选
null
与缺失的字段:MongoDB对于将一个字段设置为null
和不做任何设置这两种行为是有所区分的,这在关系型数据库中不存在。Prisma当前未体现这种区别,这意味着当筛选null
与缺失的字段时你需要特别小心。要了解更多信息,可查阅如何筛选null与缺失字段。 - 启用复制:Prisma 在内部使用MongoDB事务来避免嵌套查询上进行部分写操作。当使用事务时,MongoDB需要启用数据集的复制。要做到这点,你将需要配置一个复制集——这是一组维护相同数据集的MongoDB进程。请注意,仍然可以通过创建一个只有一个节点的副本集来使用单个数据库。如果你使用MongoDB官方的Atlas托管服务,复制集已经为你配置好了,但是如果你是在本地运行MongoDB,你需要自己设置好复制集。要了解更多信息,可查询MongoDB的部署复制集指南。
如何在Prisma中使用MongoDB
本节提供了步骤说明,是关于如何执行MongoDB所需的特定任务的。
如何迁移已存在数据以匹配Prisma schema
随着时间的推移,迁移数据库是开发周期中的一个重要部分。在开发过程中,你需要更新Prisma schema文件(例如,添加一个新字段),然后在你的开发环境数据库中更新你的数据,并最终把已更新的 schema和新的数据都推送到生产数据库。
当使用MongoDB时,请注意,你的schema和数据库之间的“耦合”是故意设计为比SQL数据库更不严格的;MongoDB 将不会强制执行schema,所以你必须验证数据的完整性。
这些更新schema的迭代任务与数据库可能会导致你的schema与数据库中的实际数据不一致。让我们看一下可能发生的一种场景,然后检查你和你的团队考虑处理这些不一致的几种策略。
场景:你需要为用户增加一个手机号码,就像电子邮箱一样。在shcema.prisma
文件中你当前的User
模型如下所示:
// prisma/schema.prisma
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String
}
这里有几种迁移的策略:
-
“按需“ 更新:在这种策略下,你和你的团队都同意可以按需更新schema。但是,为了避免因为在数据和schema之间存在数据不一致而造成迁移失败,团队必须约定任何新添加的字段都必须显式的定义为可选。
在上面的场景中,你可以在你的Prisma Schema中添加一个可选的
phoneNumber
字段到User
模型。// prisma/schema.prisma model User { id String @id @default(auto()) @map("_id") @db.ObjectId email String + phoneNumber String? }
然后使用
npx prisma generate
命令重新生成 Prisma 客户端。接着,更新你的应用以反映这个新字段,并重新部署应用。因为
phoneNumber
字段是可选的,所以你仍然可以查询那些还没有定义手机号的老的用户。当应用程序的用户开始在新字段中输入他们的手机号时,数据库里的记录将被“按需“更新另一种选择是给必填字段添加一个默认值,例如:
// prisma/schema.prisma model User { id String @id @default(auto()) @map("_id") @db.ObjectId email String + phoneNumber String @default("000-000-0000") }
然后当你遇到一个缺失的
phoneNumber
,该值会被强制转换成000-000-0000
。 -
“无重大变化“更新:此策略建立在第一个策略的基础上,你的团队进一步达成共识,即不重命名或删除字段,只添加新字段,并且始终将新字段定义为可选。可以通过在CI/CD流程中添加检查,来验证不存在向后不兼容的更改来加强此策略。
-
“一次性全部“更新:此策略与关系型数据库中的传统迁移相似,所有数据都更新以反映新的 schema。在以上的场景中,你需要创建一个脚本来给所有数据库中已存在的用户的手机号字段添加一个值。然后,你可以将该字段设置为应用程序中的必填字段,因为schema和数据是一致的。
内省之后如何添加缺失的关系
在内省一个已存在的MongoDB数据库之后,你将需要手动添加模型之间的关系。MongoDB没有通过外键定义关系的概念,就像在关系型数据库中一样。但是,如果在你的MongoDB中有一个集合带有“类似外键”字段,该字段与另一个集合的ID字段匹配,Prisma将允许你模拟集合之间的关系。
举个例子,拿一个包含User
和Post
两个集合的MongoDB数据库来说。这些集合中的数据有以下格式,带有一个userId
字段将用户和文章表连接起来:
User
集合:
_id
字段的类型是objectId
email
字段的类型是string
Post
集合:
_id
字段的类型是objectId
title
字段的类型是string
userId
字段的类型是objectId
当使用db pull
进行自省时,被拉入到Prisma schema 文件中的数据如下:
//prisma/schema.prisma
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
userId String @db.ObjectId
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String
}
这里缺失了User
和Post
模型之间的关系。要修复这个问题,需要手动给Post模型添加一个带有@relation
属性的user
字段,此属性使用userId
作为fields
的值,并给User
模型添加一个posts
字段作为反向关系:
//prisma/schema.prisma
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
userId String @db.ObjectId
+ user User @relation(fields: [userId], references: [id])
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String
+ posts Post[]
}
要了解更多如何在Prisma中使用关系的信息,请查阅相关的文档。
如何筛选null
和缺失字段
要理解MongoDB是如何区分null
和缺失字段,考虑下这个例子,一个带有name
可选字段的User
模型:
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String
name String?
}
首先,尝试创建一条name
字段显式设置为null
的记录。Prisma将返回name: null
,正如预期的那样:
const createNull = await prisma.user.create({
data: {
email: 'user1@prisma.io',
name: null,
},
})
console.log(createNull)
// CLI results
{
id: '6242c4ae032bc76da250b207',
email: 'user1@prisma.io',
name: null
}
如果你直接检查MongoDB数据库,你同样会看到一条新记录的name
设置为null
:
{
"_id": "6242c4af032bc76da250b207",
"email": "user1@prisma.io",
"name": null
}
接下来,尝试创建一条没有显式设置name
字段的记录:
const createMissing = await prisma.user.create({
data: {
email: 'user2@prisma.io',
},
})
console.log(createMissing)
// CLI results
{
id: '6242c4ae032bc76da250b208',
email: 'user2@prisma.io',
name: null
}
Prisma仍返回name: null
,但如果你直接查看数据库,你会发现这条记录完全没有定义name
字段:
{
"_id": "6242c4af032bc76da250b208",
"email": "user2@prisma.io"
}
Prisma在两种情况下返回了同样的结果,这是因为我们目前没有办法在MongoDB中指定底层数据库中为null
和完全不存的字段这两者之间的差异——要了解更多信息,可查看这个Github issue。
这意味着你目前在筛选null
和缺失字段时必须小心。用name: null
来筛选记录时将只会返回第一条记录,也就是name
被显式设置为null
的那条。
const findNulls = await prisma.user.findMany({
where: {
name: null,
},
})
console.log(findNulls)
// CLI results
[
{
id: '6242c4ae032bc76da250b207',
email: 'user1@prisma.io',
name: null
}
]
这是因为name: null
是检查是否相同,但是一个不存在的字段不等于null
。
为了也包括缺失的字段,使用isSet
过滤器来显式地搜索值为null
或未设置的字段。这将返回全部记录:
const findNullOrMissing = await prisma.user.findMany({
where: {
OR: [
{
name: null,
},
{
name: {
isSet: false,
},
},
],
},
})
console.log(findNullOrMissing)
// CLI results
[
{
id: '6242c4ae032bc76da250b207',
email: 'user1@prisma.io',
name: null
},
{
id: '6242c4ae032bc76da250b208',
email: 'user2@prisma.io',
name: null
}
]
更多MongoDB和Prisma搭配使用的实用信息
使用 MongoDB和Prisma的最快的方法是参考我们的入门文档:
这些教程将带你完成连接MongoDB、推送schema变更和使用Prisma客户端的全过程。
更多参考信息可在MongoDB连接器文档一文中查看。
要了解更多关于如何设置和管理MongoDB数据库的信息,可查阅Prisma数据指南。