跳到主要内容

Prisma ORM MongoDB 数据库连接器

本指南讨论了使用 Prisma ORM 和 MongoDB 的概念,解释了 MongoDB 与其他数据库提供商的异同,并引导您完成配置应用程序以使用 Prisma ORM 与 MongoDB 集成的过程。

信息

要将 Prisma ORM 连接到 MongoDB,请参阅我们的入门文档

什么是 MongoDB?

MongoDB 是一个 NoSQL 数据库,它以 BSON 格式存储数据,这是一种类似于 JSON 的文档格式,设计用于以键值对存储数据。它通常用于 JavaScript 应用程序开发,因为文档模型可以轻松映射到应用程序代码中的对象,并且内置了对高可用性和水平扩展的支持。

MongoDB 将数据存储在集合中,这些集合不需要像关系型数据库中的表那样预先定义模式。每个集合的结构也可以随着时间而改变。这种灵活性可以实现数据模型的快速迭代,但这意味着在使用 Prisma ORM 处理 MongoDB 数据库时,存在许多差异。

与其他数据库提供商的共通之处

使用 Prisma ORM 与 MongoDB 的某些方面与使用 Prisma ORM 与关系型数据库相同。您仍然可以:

需要考虑的差异

MongoDB 的基于文档的结构和灵活的模式意味着使用 Prisma ORM 与 MongoDB 不同于使用关系型数据库。以下是您需要注意的一些差异领域:

  • 定义 ID:MongoDB 文档有一个 _id 字段(通常包含一个 ObjectID)。Prisma ORM 不支持以 _ 开头的字段,因此需要使用 @map 属性将其映射到 Prisma ORM 字段。有关更多信息,请参阅在 MongoDB 中定义 ID

  • 迁移现有数据以匹配您的 Prisma 模式:在关系型数据库中,所有数据都必须与您的模式匹配。如果您在迁移时更改模式中特定字段的类型,则所有数据也必须更新以匹配。相反,MongoDB 不强制执行任何特定模式,因此在迁移时需要小心。有关更多信息,请参阅如何将旧数据迁移到新模式

  • 内省和 Prisma ORM 关系:当您内省现有的 MongoDB 数据库时,您将获得一个没有关系的模式,并且需要手动添加缺失的关系。有关更多信息,请参阅内省后如何添加缺失的关系

  • 过滤 null 和缺失字段:MongoDB 在将字段设置为 null 和根本不设置之间进行了区分,这在关系型数据库中不存在。Prisma ORM 目前不表达这种区分,这意味着在过滤 null 和缺失字段时需要小心。有关更多信息,请参阅如何过滤 null 和缺失字段

  • 启用复制:Prisma ORM 在内部使用 MongoDB 事务,以避免嵌套查询中的部分写入。使用事务时,MongoDB 要求启用数据集的复制。为此,您需要配置一个 副本集 —— 这是一组维护相同数据集的 MongoDB 进程。请注意,通过创建一个只有一个节点的副本集,仍然可以使用单个数据库。如果您使用 MongoDB 的 Atlas 托管服务,副本集已为您配置,但如果您在本地运行 MongoDB,则需要自己设置副本集。有关更多信息,请参阅 MongoDB 的部署副本集指南

大型集合的性能考虑

问题

通过 Prisma 处理大型 MongoDB 集合时,某些操作可能会变得缓慢且占用资源。特别是,需要扫描整个集合的操作(例如 count())可能会达到查询执行时间限制,并随着数据集的增长显着影响性能。

解决方案

要解决大型 MongoDB 集合的性能问题,请考虑以下方法:

  1. 对于大型集合,请考虑使用 MongoDB 的 estimatedDocumentCount() 而不是 count()。此方法速度快得多,因为它使用有关集合的元数据。您可以使用 Prisma 的 runCommandRaw 方法执行此命令。

  2. 对于频繁访问的计数,请考虑实现一个计数器缓存。这涉及维护一个单独的文档,其中包含预先计算的计数,您可以在添加或删除文档时更新这些计数。

如何使用 Prisma ORM 与 MongoDB

本节提供了如何执行需要特定于 MongoDB 的步骤的任务的说明。

如何迁移现有数据以匹配您的 Prisma 模式

随着时间的推移迁移数据库是开发周期的重要组成部分。在开发过程中,您需要更新您的 Prisma 模式(例如,添加新字段),然后更新开发环境数据库中的数据,并最终将更新后的模式和新数据推送到生产数据库。

信息

使用 MongoDB 时,请注意您的模式与数据库之间的“耦合”特意设计得比 SQL 数据库不那么严格;MongoDB 不会强制执行模式,因此您必须验证数据完整性。

这些更新模式和数据库的迭代任务可能导致模式与数据库中的实际数据之间出现不一致。让我们来看一个可能发生这种情况的场景,然后研究几种供您和您的团队考虑的策略来处理这些不一致。

场景:您需要为用户添加电话号码和电子邮件。您当前的 schema.prisma 文件中包含以下 User 模型:

prisma/schema.prisma
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String
}

您可以采用多种策略来迁移此模式:

  • “按需”更新:采用此策略,您和您的团队已同意可以根据需要对模式进行更新。但是,为了避免由于数据和模式之间的不一致而导致的迁移失败,团队中达成一致,任何新增字段都明确定义为可选字段。

    在上述场景中,您可以在 Prisma 模式的 User 模型中添加一个可选的 phoneNumber 字段:

    prisma/schema.prisma
    model User {
    id String @id @default(auto()) @map("_id") @db.ObjectId
    email String
    phoneNumber String?
    }

    然后使用 npx prisma generate 命令重新生成您的 Prisma Client。

    接下来,更新您的应用程序以反映新字段,并重新部署您的应用程序。

    由于 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 过程中添加检查来加强,以验证模式没有向后不兼容的更改。

  • “一次性”更新:此策略类似于关系型数据库中的传统迁移,所有数据都会更新以反映新模式。在上述场景中,您将创建一个脚本,为数据库中所有现有用户的电话号码字段添加一个值。然后,您可以在应用程序中将该字段设置为必填字段,因为模式和数据是一致的。

内省后如何添加缺失的关系

在内省现有 MongoDB 数据库后,您需要手动添加模型之间的关系。MongoDB 没有通过外键定义关系的概念,就像在关系型数据库中一样。但是,如果您的 MongoDB 集合中有一个“类似外键”的字段与另一个集合的 ID 字段匹配,Prisma ORM 将允许您模拟集合之间的关系。

例如,假设一个 MongoDB 数据库包含两个集合:UserPost。这些集合中的数据具有以下格式,其中 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
}

这缺少 UserPost 模型之间的关系。要解决此问题,请手动向 Post 模型添加一个 user 字段,并使用 @relation 属性,将 userId 作为 fields 值,将其链接到 User 模型,并向 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 ORM 中使用关系的更多信息,请参阅我们的文档

如何过滤 null 和缺失字段

为了理解 MongoDB 如何区分 null 和缺失字段,考虑一个带有可选 name 字段的 User 模型的示例:

model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String
name String?
}

首先,尝试创建一个 name 字段明确设置为 null 的记录。Prisma ORM 将按预期返回 name: null

const createNull = await prisma.user.create({
data: {
email: 'user1@prisma.io',
name: null,
},
})
console.log(createNull)
显示CLI结果
{
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结果
{
id: '6242c4ae032bc76da250b208',
email: 'user2@prisma.io',
name: null
}

Prisma ORM 仍然返回 name: null,但如果您直接查看数据库,您会看到该记录根本没有定义 name 字段:

{
"_id": "6242c4af032bc76da250b208",
"email": "user2@prisma.io"
}

Prisma ORM 在两种情况下都返回相同的结果,因为我们目前无法在 MongoDB 中指定底层数据库中为 null 的字段与根本未定义的字段之间的这种差异——有关更多信息,请参阅此 Github 问题

这意味着您目前在过滤 null 和缺失字段时必须小心。过滤 name: null 的记录只会返回第一条记录,其中 name 明确设置为 null

const findNulls = await prisma.user.findMany({
where: {
name: null,
},
})
console.log(findNulls)
显示CLI结果
[
{
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结果
[
{
id: '6242c4ae032bc76da250b207',
email: 'user1@prisma.io',
name: null
},
{
id: '6242c4ae032bc76da250b208',
email: 'user2@prisma.io',
name: null
}
]

更多关于使用 Prisma ORM 和 MongoDB 的信息

开始使用 Prisma ORM 和 MongoDB 的最快方法是查阅我们的入门文档:

这些教程将引导您完成连接到 MongoDB、推送模式更改以及使用 Prisma Client 的过程。

更多参考信息可在MongoDB 连接器文档中找到。

有关如何设置和管理 MongoDB 数据库的更多信息,请参阅 Prisma 数据指南

示例

要连接到 MongoDB 服务器,请在您的 Prisma Schema 中配置 datasource 块:

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

传递给 datasource 块的字段是:

  • provider:指定 mongodb 数据源连接器。
  • url:指定 MongoDB 服务器的连接 URL。在这种情况下,使用环境变量 提供连接 URL。
警告

MongoDB 数据库连接器使用事务来支持嵌套写入。事务需要副本集部署。部署副本集的最简单方法是使用 Atlas。它提供免费入门。

连接详情

连接 URL

MongoDB 连接 URL 可以根据您托管数据库的方式以不同的方式配置。标准配置由以下组件组成:

Structure of the MongoDB connection URL

基本 URL 和路径

连接 URL 的基本 URL 和路径部分由您的身份验证凭据、主机(以及可选的端口号)和数据库组成。

mongodb://USERNAME:PASSWORD@HOST/DATABASE

以下组件构成了数据库的基本 URL

名称占位符描述
用户USERNAME您的数据库用户名,例如 janedoe
密码PASSWORD您的数据库用户的密码
主机HOST运行 mongod 实例的主机。如果您运行的是分片集群,则这将是 mongos 实例。这可以是主机名、IP 地址或 UNIX 域套接字。
端口PORT您的数据库服务器运行的端口,例如 1234。如果未提供,则使用默认值 27017
数据库DATABASE要使用的数据库名称。如果未指定,但设置了 authSource 选项,则使用 authSource 数据库名称。如果连接字符串中的数据库和 authSource 选项均未指定,则默认为 admin

参数

连接 URL 还可以接受参数。以下示例设置了三个参数:

  • ssl 连接
  • connectTimeoutMS
  • 以及 maxPoolSize
mongodb://USERNAME:PASSWORD@HOST/DATABASE?ssl=true&connectTimeoutMS=5000&maxPoolSize=50

有关连接字符串参数的完整列表,请参阅 MongoDB 连接字符串文档。没有 Prisma ORM 特定的参数。

使用 ObjectId

MongoDB 文档的 _id 字段包含 ObjectId 是一种常见做法:

{
"_id": { "$oid": "60d599cb001ef98000f2cad2" },
"createdAt": { "$date": { "$numberLong": "1624611275577" } },
"email": "ella@prisma.io",
"name": "Ella",
"role": "ADMIN"
}

任何映射到底层数据库中 ObjectId 的字段(最常见的是 ID 和关系标量字段):

  • 必须是 StringBytes 类型
  • 必须包含 @db.ObjectId 属性
  • 可以选择使用 @default(auto()) 在文档创建时自动生成有效的 ObjectId

这是一个使用 String 的例子:

model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
// Other fields
}

这是另一个使用 Bytes 的例子:

model User {
id Bytes @id @default(auto()) @map("_id") @db.ObjectId
// Other fields
}

另请参阅:在 MongoDB 中定义 ID 字段

生成 ObjectId

要在您的应用程序中生成有效的 ObjectId(用于测试目的或手动设置 ID 字段值),请使用 bson 包。

npm install --save bson
import { ObjectId } from 'bson'

const id = new ObjectId()

与关系型数据库连接器的差异

本节介绍了 MongoDB 连接器与 Prisma ORM 关系型数据库连接器的不同之处。

不支持 Prisma Migrate

目前,没有计划添加对 Prisma Migrate 的支持,因为 MongoDB 项目不依赖于需要使用额外工具管理更改的内部模式。@unique 索引的管理通过 db push 实现。

不支持 @@idautoincrement()

不支持 @@id 属性(多个字段的 ID),因为 MongoDB 中的主键始终位于模型的 _id 字段上。

不支持 autoincrement() 函数(创建自增 @id 值),因为 autoincrement() 不适用于 MongoDB 中 _id 字段的 ObjectID 类型。

循环引用和引用操作

如果您的模型中存在循环引用(无论是自引用还是模型之间关系的循环),并且您使用引用操作,则必须将引用操作设置为 NoAction 以防止无限循环。

有关更多详细信息,请参阅引用操作的特殊规则

副本集配置

MongoDB 仅允许在副本集上启动事务。Prisma ORM 内部使用事务来避免嵌套查询中的部分写入。这意味着我们继承了需要配置副本集的要求。

当您尝试在未配置副本集的部署上使用 Prisma ORM 的 MongoDB 连接器时,Prisma ORM 会显示消息 Error: Transactions are not supported by this deployment。错误消息的完整文本如下:

PrismaClientUnknownRequestError2 [PrismaClientUnknownRequestError]:
Invalid `prisma.post.create()` invocation in
/index.ts:9:21

6 await prisma.$connect()
7
8 // Create the first post
→ 9 await prisma.post.create(
Error in connector: Database error. error code: unknown, error message: Transactions are not supported by this deployment
at cb (/node_modules/@prisma/client/runtime/index.js:34804:17)
at processTicksAndRejections (internal/process/task_queues.js:97:5) {
clientVersion: '3.xx.0'
}

要解决此问题,我们建议您将部署更改为已配置副本集的部署。

一个简单的方法是使用 MongoDB Atlas 来启动一个免费实例,该实例开箱即支持副本集。

还有一种选项可以使用此指南在本地运行副本集:https://mongodb.ac.cn/docs/manual/tutorial/convert-standalone-to-replica-set

MongoDB 与 Prisma 模式之间的类型映射

MongoDB 连接器将 Prisma ORM 数据模型中的标量类型映射到 MongoDB 的原生列类型,如下所示:

或者,请参阅Prisma 模式参考,了解按 Prisma 类型组织的类型映射。

从 Prisma ORM 到 MongoDB 的原生类型映射

Prisma ORMMongoDB
Stringstring
Booleanbool
Intint
BigIntlong
Floatdouble
Decimal目前不支持
DateTimetimestamp
BytesbinData
Json

目前不支持的 MongoDB 类型

  • Decimal128
  • Undefined
  • DBPointer
  • Null
  • Symbol
  • MinKey
  • MaxKey
  • Object
  • Javascript
  • JavascriptWithScope
  • Regex

内省时从 MongoDB 到 Prisma ORM 类型的映射

在内省 MongoDB 数据库时,Prisma ORM 使用相关的标量类型。某些特殊类型还会获得额外的原生类型注释:

MongoDB (类型 | 别名)Prisma ORM支持原生数据库类型属性备注
objectIdString✔️@db.ObjectId

内省尚不支持的原生数据库类型添加为 Unsupported 字段

schema.prisma
model Example {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
regex Unsupported("RegularExpression")
}
© . All rights reserved.