跳到主要内容

MongoDB

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

信息

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

什么是 MongoDB?

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

MongoDB 将数据存储在集合中,无需像关系型数据库那样提前定义 schema。每个集合的结构也可以随着时间推移而改变。这种灵活性可以加速数据模型的迭代,但这确实意味着在使用 Prisma ORM 处理 MongoDB 数据库时会存在一些差异。

与其他数据库提供商的共同点

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

需要考虑的差异

MongoDB 基于文档的结构和灵活的 schema 意味着使用 Prisma ORM 处理 MongoDB 在许多方面与处理关系型数据库有所不同。以下是一些您需要注意的差异之处

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

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

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

  • 过滤 null 和缺失字段:MongoDB 区分将字段设置为 null 和完全不设置字段,这在关系型数据库中不存在。Prisma ORM 目前无法表达这种区别,这意味着您在过滤 null 和缺失字段时需要小心。更多信息请参阅 如何过滤 null 和缺失字段

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

大型集合的性能注意事项

问题

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

解决方案

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

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

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

如何将 Prisma ORM 与 MongoDB 结合使用

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

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

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

信息

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

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

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

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

有几种可以用于迁移此 schema 的策略

  • “按需”更新:采用此策略,您和您的团队一致同意可以根据需要对 schema 进行更新。然而,为了避免由于数据与 schema 不一致导致的迁移失败,团队内部达成一致:任何新添加的字段都明确定义为可选字段。

    在我们上面的场景中,您可以在 Prisma schema 的 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 流程中添加检查来验证 schema 没有向后不兼容的更改,从而强化此策略。

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

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

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

例如,假设一个 MongoDB 数据库有两个集合:UserPost。这些集合中的数据格式如下,其中 userId 字段将用户与帖子关联起来

User 集合

  • 类型为 objectId_id 字段
  • 类型为 stringemail 字段

Post 集合

  • 类型为 objectId_id 字段
  • 类型为 stringtitle 字段
  • 类型为 objectIDuserId 字段

使用 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 模型之间的关系。要解决此问题,请手动将一个 user 字段添加到 Post 模型中,并使用 @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
}
]

更多关于将 MongoDB 与 Prisma ORM 结合使用

开始将 MongoDB 与 Prisma ORM 结合使用的最快方法是参考我们的入门文档

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

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

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

示例

要连接到 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 项目不依赖于需要额外工具管理的内部 schema。@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 Schema 之间的类型映射

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

或者,请参阅Prisma Schema 参考,了解按 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")
}