跳到主要內容

MongoDB

本指南討論了使用 Prisma ORM 和 MongoDB 背後的概念,解釋了 MongoDB 與其他資料庫供應商之間的共通點和差異,並引導您完成設定應用程式以使用 Prisma ORM 與 MongoDB 整合的過程。

資訊

若要將 Prisma ORM 連接到 MongoDB,請參閱我們的開始使用文件

什麼是 MongoDB?

MongoDB 是一種 NoSQL 資料庫,以 BSON 格式儲存資料,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 不強制執行任何特定的結構描述,因此您在遷移時需要小心。如需更多資訊,請參閱如何遷移舊資料以符合新的結構描述

  • Introspection 和 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 將允許您模擬集合之間的關係。

舉例來說,假設有一個包含兩個集合 UserPost 的 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
}

這遺失了 UserPost 模型之間的關係。若要修正此問題,請手動將 user 欄位新增到 Post 模型,並使用 @relation 屬性,將 userId 作為 fields 值,將其連結到 User 模型,並將 posts 欄位新增到 User 模型作為反向關係

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 一起使用的更多資訊

開始使用 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

名稱佔位符描述
使用者使用者名稱您的資料庫使用者的名稱,例如 janedoe
密碼密碼您的資料庫使用者的密碼
主機主機執行 mongod 執行個體的主機。如果您正在執行分片叢集,這將是 mongos 執行個體。這可以是主機名稱、IP 位址或 UNIX 網域套接字。
您的資料庫伺服器執行的埠,例如 1234。如果未提供,則使用預設值 27017
資料庫資料庫要使用的資料庫名稱。如果未指定,但設定了 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://www.mongodb.com/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 types that are currently unsupported

  • 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")
}