Express 教程第三部分:使用資料庫(Mongoose)
本文簡要介紹了資料庫以及如何在 Node/Express 應用程式中使用它們。然後繼續展示瞭如何使用 Mongoose 為 LocalLibrary 網站提供資料庫訪問。它解釋瞭如何宣告物件 schema 和模型、主要欄位型別以及基本驗證。它還簡要展示了訪問模型資料的幾種主要方法。
| 預備知識 | Express 教程第二部分:建立一個骨架網站 |
|---|---|
| 目標 | 能夠使用 Mongoose 設計和建立自己的模型。 |
概述
圖書館工作人員將使用本地圖書館網站儲存有關書籍和借閱者的資訊,而圖書館會員將使用它瀏覽和搜尋書籍,瞭解是否有可用副本,然後預訂或借閱它們。為了高效地儲存和檢索資訊,我們將把它們儲存在資料庫中。
Express 應用程式可以使用許多不同的資料庫,並且您可以使用幾種方法來執行Create(建立)、Read(讀取)、Update(更新)和Delete(刪除)(CRUD) 操作。本教程簡要概述了一些可用選項,然後詳細介紹了所選的特定機制。
我可以使用哪些資料庫?
Express 應用程式可以使用 Node 支援的任何資料庫(Express 本身不定義任何特定的資料庫管理附加行為/要求)。有許多流行選項,包括 PostgreSQL、MySQL、Redis、SQLite 和 MongoDB。
在選擇資料庫時,您應該考慮諸如生產力/學習曲線時間、效能、複製/備份的便捷性、成本、社群支援等因素。雖然沒有單一的“最佳”資料庫,但對於我們 Local Library 這樣的小型到中型網站來說,幾乎所有流行的解決方案都應該足夠。
有關選項的更多資訊,請參閱資料庫整合 (Express 文件)。
與資料庫互動的最佳方式是什麼?
與資料庫互動有兩種常見方法
- 使用資料庫的原生查詢語言,例如 SQL。
- 使用物件關係對映器(“ORM”)或物件文件對映器(“ODM”)。它們將網站資料表示為 JavaScript 物件,然後將其對映到底層資料庫。一些 ORM 和 ODM 與特定資料庫繫結,而另一些則提供與資料庫無關的後端。
使用 SQL 或資料庫支援的任何查詢語言可以獲得最佳效能。物件對映器通常較慢,因為它們使用轉換程式碼在物件和資料庫格式之間進行對映,這可能無法使用最有效的資料庫查詢(如果對映器支援不同的資料庫後端,並且必須在支援的資料庫功能方面做出更大的妥協,則尤其如此)。
使用 ORM/ODM 的好處是,程式設計師可以繼續以 JavaScript 物件的術語而不是資料庫語義進行思考——如果您需要使用不同的資料庫(在相同或不同的網站上),這一點尤其如此。它們還提供了一個執行資料驗證的明確位置。
注意: 使用 ODM/ORM 通常會降低開發和維護成本!除非您非常熟悉原生查詢語言或效能至關重要,否則您應該強烈考慮使用 ODM。
我應該使用哪個 ORM/ODM?
npm 包管理器網站上提供了許多 ODM/ORM 解決方案(檢視 odm 和 orm 標籤以獲取子集!)。
在撰寫本文時流行的一些解決方案是
- Mongoose:Mongoose 是一個 MongoDB 物件建模工具,旨在非同步環境中工作。
- Waterline:一個從基於 Express 的 Sails Web 框架中提取的 ORM。它提供了統一的 API 來訪問許多不同的資料庫,包括 Redis、MySQL、LDAP、MongoDB 和 Postgres。
- Bookshelf:具有基於 Promise 和傳統回撥介面,提供事務支援、急切/巢狀急切關係載入、多型關聯以及對一對一、一對多和多對多關係的支援。適用於 PostgreSQL、MySQL 和 SQLite3。
- Objection:儘可能簡化 SQL 和底層資料庫引擎的全部功能的使用(支援 SQLite3、Postgres 和 MySQL)。
- Sequelize 是一個基於 Promise 的 Node.js 和 io.js ORM。它支援 PostgreSQL、MySQL、MariaDB、SQLite 和 MSSQL 方言,並具有強大的事務支援、關係、讀複製等功能。
- Node ORM2 是 NodeJS 的物件關係管理器。它支援 MySQL、SQLite 和 Postgres,有助於使用面向物件的方法處理資料庫。
- GraphQL:主要是一種用於 RESTful API 的查詢語言,GraphQL 非常流行,並且具有從資料庫讀取資料的功能。
通常,在選擇解決方案時,您應該同時考慮所提供的功能和“社群活動”(下載量、貢獻量、錯誤報告、文件質量等)。在撰寫本文時,Mongoose 是迄今為止最流行的 ODM,如果您正在將 MongoDB 用作資料庫,它是一個合理的選擇。
將 Mongoose 和 MongoDB 用於 LocalLibrary
對於 Local Library 示例(以及本主題的其餘部分),我們將使用 Mongoose ODM 來訪問我們的圖書館資料。Mongoose 作為 MongoDB 的前端,MongoDB 是一個開源的 NoSQL 資料庫,它使用面向文件的資料模型。MongoDB 資料庫中的“文件集合”類似於關係資料庫中的“表的行”。
這種 ODM 和資料庫組合在 Node 社群中非常流行,部分原因是文件儲存和查詢系統看起來非常像 JSON,因此 JavaScript 開發人員很熟悉。
注意: 您不需要了解 MongoDB 才能使用 Mongoose,儘管如果您已經熟悉 MongoDB,Mongoose 文件的某些部分會更容易使用和理解。
本教程的其餘部分展示瞭如何為 LocalLibrary 網站示例定義和訪問 Mongoose schema 和模型。
設計 LocalLibrary 模型
在您開始編寫模型程式碼之前,花幾分鐘時間思考我們需要儲存哪些資料以及不同物件之間的關係是值得的。
我們知道我們需要儲存書籍資訊(標題、摘要、作者、型別、ISBN),並且我們可能有多個可用副本(具有全域性唯一 ID、可用性狀態等)。我們可能需要儲存比作者姓名更多的作者資訊,並且可能存在多個姓名相同或相似的作者。我們希望能夠根據書名、作者、型別和類別對資訊進行排序。
在設計模型時,為每個“物件”(一組相關資訊)建立單獨的模型是有意義的。在這種情況下,這些模型的一些明顯候選者是書籍、書籍例項和作者。
您可能還希望使用模型來表示選擇列表選項(例如,像下拉選擇列表),而不是將選擇硬編碼到網站本身——當所有選項未提前知曉或可能更改時,建議這樣做。一個很好的例子是型別(例如,奇幻、科幻等)。
一旦我們確定了模型和欄位,我們需要考慮它們之間的關係。
考慮到這一點,下面的 UML 關聯圖顯示了我們將在此案例中定義的模型(用方框表示)。如上所述,我們為書籍(書籍的通用詳細資訊)、書籍例項(系統中可用書籍特定物理副本的狀態)和作者建立了模型。我們還決定為型別建立一個模型,以便可以動態建立值。我們決定不為 BookInstance:status 建立模型——我們將硬編碼可接受的值,因為我們不期望這些值會改變。在每個方框中,您可以看到模型名稱、欄位名稱和型別,以及方法及其返回型別。
該圖還顯示了模型之間的關係,包括它們的多重性。多重性是圖上的數字,顯示關係中可能存在的每個模型的數量(最大值和最小值)。例如,方框之間的連線線顯示 Book 和 Genre 是相關的。靠近 Book 模型的數字顯示一個 Genre 必須有零個或多個 Book(您可以根據需要擁有任意數量),而線另一端靠近 Genre 的數字顯示一本書可以有零個或多個關聯的 Genre。
注意: 如我們下面 Mongoose 簡介 中所討論,通常最好將定義文件/模型之間關係的欄位放在一個模型中(您仍然可以透過在另一個模型中搜索關聯的 _id 來找到反向關係)。下面我們選擇在 Book schema 中定義 Book/Genre 和 Book/Author 之間的關係,在 BookInstance Schema 中定義 Book/BookInstance 之間的關係。這個選擇有些隨意——我們也可以將欄位放在另一個 schema 中。

注意: 下一節提供了一個基本簡介,解釋瞭如何定義和使用模型。閱讀時,請考慮我們將如何構建上面圖中的每個模型。
資料庫 API 是非同步的
用於建立、查詢、更新或刪除記錄的資料庫方法是非同步的。這意味著這些方法會立即返回,而處理方法成功或失敗的程式碼將在操作完成後的某個時間執行。當伺服器等待資料庫操作完成時,其他程式碼可以執行,因此伺服器可以保持對其他請求的響應。
JavaScript 有許多支援非同步行為的機制。歷史上,JavaScript 嚴重依賴將回撥函式傳遞給非同步方法來處理成功和錯誤情況。在現代 JavaScript 中,回撥已基本被Promise取代。Promise 是非同步方法(立即)返回的物件,表示其未來狀態。當操作完成時,Promise 物件“已解決”,並解析一個表示操作結果或錯誤的物件。
當 Promise 解決時,有兩種主要方法可以使用 Promise 來執行程式碼,我們強烈建議您閱讀如何使用 Promise 以獲取兩種方法的高階概述。在本教程中,我們將主要使用await在async function中等待 Promise 完成,因為這會使非同步程式碼更具可讀性和可理解性。
這種方法的原理是,您使用 async function 關鍵字將函式標記為非同步函式,然後在該函式內部將 await 應用於任何返回 Promise 的方法。當非同步函式執行時,其操作在第一個 await 方法處暫停,直到 Promise 解決。從周圍程式碼的角度來看,非同步函式然後返回,並且其後的程式碼能夠執行。稍後當 Promise 解決時,非同步函式內部的 await 方法返回結果,如果 Promise 被拒絕,則會丟擲錯誤。然後,非同步函式中的程式碼將執行,直到遇到另一個 await(此時它將再次暫停),或者直到函式中的所有程式碼都已執行。
您可以在下面的示例中看到它的工作原理。myFunction() 是一個在try...catch塊中呼叫的非同步函式。當 myFunction() 執行時,程式碼執行在 methodThatReturnsPromise() 處暫停,直到 Promise 解決,此時程式碼繼續執行到 functionThatReturnsPromise() 並再次等待。如果在非同步函式中丟擲錯誤,catch 塊中的程式碼將執行,如果任何方法返回的 Promise 被拒絕,就會發生這種情況。
async function myFunction() {
// …
await someObject.methodThatReturnsPromise();
// …
await functionThatReturnsPromise();
// …
}
try {
// …
myFunction();
// …
} catch (e) {
// error handling code
}
上面的非同步方法按順序執行。如果方法之間不相互依賴,則可以並行執行它們,從而更快地完成整個操作。這透過使用 Promise.all() 方法來完成,該方法將可迭代的 Promise 作為輸入並返回單個 Promise。當所有輸入的 Promise 都完成時,此返回的 Promise 將完成,並帶有一個包含完成值的陣列。當任何輸入的 Promise 被拒絕時,它將拒絕,並帶上第一個拒絕原因。
下面的程式碼演示了其工作原理。首先,我們有兩個返回 Promise 的函式。我們 await 它們透過 Promise.all() 返回的 Promise 完成。一旦它們都完成,await 返回並填充結果陣列,函式然後繼續到下一個 await,並等待 anotherFunctionThatReturnsPromise() 返回的 Promise 解決。您將在 try...catch 塊中呼叫 myFunction() 以捕獲任何錯誤。
async function myFunction() {
// …
const [resultFunction1, resultFunction2] = await Promise.all([
functionThatReturnsPromise1(),
functionThatReturnsPromise2(),
]);
// …
await anotherFunctionThatReturnsPromise(resultFunction1);
}
帶有 await/async 的 Promise 允許對非同步執行進行靈活且“可理解”的控制!
Mongoose 簡介
本節概述瞭如何將 Mongoose 連線到 MongoDB 資料庫,如何定義 schema 和模型,以及如何進行基本查詢。
注意: 本簡介深受 npm 上的 Mongoose 快速入門 和 官方文件 的影響。
安裝 Mongoose 和 MongoDB
Mongoose 像任何其他依賴項一樣安裝在您的專案 (package.json) 中——使用 npm。要在專案資料夾中安裝它,請使用以下命令
npm install mongoose
安裝 Mongoose 會新增其所有依賴項,包括 MongoDB 資料庫驅動程式,但它不會安裝 MongoDB 本身。如果您想安裝 MongoDB 伺服器,則可以在此處為各種作業系統下載安裝程式並在本地安裝。您也可以使用基於雲的 MongoDB 例項。
注意: 對於本教程,我們將使用 MongoDB Atlas 雲託管的資料庫即服務免費層來提供資料庫。這適用於開發,並且對於本教程來說是合理的,因為它使“安裝”與作業系統無關(資料庫即服務也是您可能用於生產資料庫的一種方法)。
連線到 MongoDB
Mongoose 需要連線到 MongoDB 資料庫。您可以 require() 並使用 mongoose.connect() 連線到本地託管的資料庫,如下所示(對於本教程,我們將改為連線到網際網路託管的資料庫)。
// Import the mongoose module
const mongoose = require("mongoose");
// Set `strictQuery: false` to globally opt into filtering by properties that aren't in the schema
// Included because it removes preparatory warnings for Mongoose 7.
// See: https://mongoosejs.com/docs/migrating_to_6.html#strictquery-is-removed-and-replaced-by-strict
mongoose.set("strictQuery", false);
// Define the database URL to connect to.
const mongoDB = "mongodb://127.0.0.1/my_database";
// Wait for database to connect, logging an error if there is a problem
main().catch((err) => console.log(err));
async function main() {
await mongoose.connect(mongoDB);
}
注意: 如 資料庫 API 是非同步的 部分所述,這裡我們在 async 函式中 await connect() 方法返回的 Promise。我們使用 Promise 的 catch() 處理程式來處理連線時可能發生的任何錯誤,但我們也可以在 try...catch 塊中呼叫 main()。
您可以透過 mongoose.connection 獲取預設的 Connection 物件。如果您需要建立額外的連線,可以使用 mongoose.createConnection()。它採用與 connect() 相同形式的資料庫 URI(包含主機、資料庫、埠、選項等)並返回一個 Connection 物件)。請注意,createConnection() 會立即返回;如果您需要等待連線建立,可以呼叫 asPromise() 來返回一個 Promise(mongoose.createConnection(mongoDB).asPromise())。
定義和建立模型
模型使用 Schema 介面定義。Schema 允許您定義儲存在每個文件中的欄位及其驗證要求和預設值。此外,您可以定義靜態和例項輔助方法,以便更輕鬆地處理資料型別,以及可以像任何其他欄位一樣使用的虛擬屬性,但它們實際上並不儲存在資料庫中(我們將在下面進一步討論)。
然後使用 mongoose.model() 方法將 Schema “編譯”成模型。一旦有了模型,您就可以使用它來查詢、建立、更新和刪除給定型別的物件。
注意: 每個模型都對映到 MongoDB 資料庫中的文件集合。文件將包含模型 Schema 中定義的欄位/Schema 型別。
定義 Schema
下面的程式碼片段展示瞭如何定義一個簡單的 schema。首先,您 require() mongoose,然後使用 Schema 建構函式建立一個新的 schema 例項,並在建構函式的物件引數中定義各種欄位。
// Require Mongoose
const mongoose = require("mongoose");
// Define a schema
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date,
});
在上面的例子中,我們只有兩個欄位,一個字串和一個日期。在下一節中,我們將展示一些其他欄位型別、驗證和其他方法。
建立模型
模型是使用 mongoose.model() 方法從 Schema 建立的
// Define schema
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date,
});
// Compile model from schema
const SomeModel = mongoose.model("SomeModel", SomeModelSchema);
第一個引數是為模型建立的集合的單數名稱(Mongoose 將為上面SomeModel模型建立資料庫集合),第二個引數是您希望在建立模型時使用的 Schema。
注意: 一旦您定義了模型類,您就可以使用它們來建立、更新或刪除記錄,並執行查詢以獲取所有記錄或特定記錄子集。我們將在使用模型部分以及建立檢視時向您展示如何執行此操作。
Schema 型別(欄位)
一個 Schema 可以有任意數量的欄位——每個欄位代表 MongoDB 中儲存的文件中的一個欄位。下面顯示了一個示例 Schema,展示了許多常見的欄位型別以及它們的宣告方式。
const schema = new Schema({
name: String,
binary: Buffer,
living: Boolean,
updated: { type: Date, default: Date.now() },
age: { type: Number, min: 18, max: 65, required: true },
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
array: [],
ofString: [String], // You can also have an array of each of the other types too.
nested: { stuff: { type: String, lowercase: true, trim: true } },
});
大多數 SchemaTypes(“type:”或欄位名稱之後的描述符)都是不言自明的。例外情況是
ObjectId:表示資料庫中模型的特定例項。例如,一本書可以使用它來表示其作者物件。這實際上將包含指定物件的唯一 ID (_id)。我們可以在需要時使用populate()方法來引入關聯資訊。Mixed:任意 schema 型別。[]:專案陣列。您可以在這些模型上執行 JavaScript 陣列操作(push、pop、unshift 等)。上面的示例顯示了一個沒有指定型別的物件陣列和一個String物件陣列,但您可以擁有任何型別的物件陣列。
程式碼還顯示了兩種宣告欄位的方式
- 欄位名稱和型別作為鍵值對(即,像欄位
name、binary和living那樣)。 - 欄位名稱後跟一個物件,該物件定義了欄位的
type和任何其他選項。選項包括:- 預設值。
- 內建驗證器(例如,最大/最小值)和自定義驗證函式。
- 該欄位是否為必需欄位
String欄位是否應自動設定為小寫、大寫或修剪(例如,{ type: String, lowercase: true, trim: true })
有關選項的更多資訊,請參閱 SchemaTypes (Mongoose 文件)。
驗證
Mongoose 提供內建和自定義驗證器,以及同步和非同步驗證器。它允許您在所有情況下指定可接受的值範圍和驗證失敗的錯誤訊息。
內建驗證器包括
以下示例(略微修改自 Mongoose 文件)展示瞭如何指定某些驗證器型別和錯誤訊息
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, "Too few eggs"],
max: 12,
required: [true, "Why no eggs?"],
},
drink: {
type: String,
enum: ["Coffee", "Tea", "Water"],
},
});
有關欄位驗證的完整資訊,請參閱 驗證 (Mongoose 文件)。
虛擬屬性
虛擬屬性是您可以獲取和設定的文件屬性,但它們不會持久化到 MongoDB。getter 對於格式化或組合欄位很有用,而 setter 對於將單個值分解為多個值以進行儲存很有用。文件中的示例從名字和姓氏欄位構建(和解構)一個全名虛擬屬性,這比每次在模板中使用時都構建全名更容易、更簡潔。
注意: 我們將在庫中使用一個虛擬屬性,透過路徑和記錄的 _id 值來為每個模型記錄定義一個唯一的 URL。
有關更多資訊,請參閱 虛擬屬性 (Mongoose 文件)。
方法和查詢助手
一個 schema 還可以有例項方法、靜態方法和查詢助手。例項方法和靜態方法類似,但顯而易見的區別是例項方法與特定記錄相關聯並可以訪問當前物件。查詢助手允許您擴充套件 mongoose 的鏈式查詢構建器 API(例如,除了 find()、findOne() 和 findById() 方法之外,還可以新增查詢“byName”)。
使用模型
一旦建立了一個 schema,就可以使用它來建立模型。模型代表資料庫中可搜尋的文件集合,而模型的例項代表您可以儲存和檢索的單個文件。
我們將在下面提供一個簡要概述。有關更多資訊,請參閱:模型 (Mongoose 文件)。
注意: 記錄的建立、更新、刪除和查詢都是非同步操作,會返回一個 Promise。下面的示例僅展示了相關方法的用法和 await(即,使用方法的核心程式碼)。為了清晰起見,省略了用於捕獲錯誤的外部 async function 和 try...catch 塊。有關使用 await/async 的更多資訊,請參閱上面的 資料庫 API 是非同步的。
建立和修改文件
要建立記錄,您可以定義模型的例項,然後對其呼叫 save()。下面的示例假設 SomeModel 是我們從 schema 建立的模型(帶有一個欄位 name)。
// Create an instance of model SomeModel
const awesome_instance = new SomeModel({ name: "awesome" });
// Save the new model instance asynchronously
await awesome_instance.save();
您還可以使用 create() 在儲存模型例項的同時定義它。下面我們只建立一個,但您可以透過傳入物件陣列來建立多個例項。
await SomeModel.create({ name: "also_awesome" });
每個模型都有一個關聯的連線(當您使用 mongoose.model() 時,這將是預設連線)。您可以建立一個新連線並在其上呼叫 .model() 以在不同的資料庫上建立文件。
您可以使用點語法訪問此新記錄中的欄位,並更改值。您必須呼叫 save() 或 update() 才能將修改後的值儲存回資料庫。
// Access model field values using dot notation
console.log(awesome_instance.name); // should log 'also_awesome'
// Change record by modifying the fields, then calling save().
awesome_instance.name = "New cool name";
await awesome_instance.save();
搜尋記錄
您可以使用查詢方法搜尋記錄,將查詢條件指定為 JSON 文件。下面的程式碼片段展示瞭如何查詢資料庫中所有打網球的運動員,只返回運動員的姓名和年齡欄位。這裡我們只指定一個匹配欄位(運動),但您可以新增更多條件,指定正則表示式條件,或者完全刪除條件以返回所有運動員。
const Athlete = mongoose.model("Athlete", yourSchema);
// find all athletes who play tennis, returning the 'name' and 'age' fields
const tennisPlayers = await Athlete.find(
{ sport: "Tennis" },
"name age",
).exec();
注意: 重要的是要記住,未找到任何結果對於搜尋來說不是錯誤——但在應用程式的上下文中可能是一個失敗情況。如果您的應用程式期望搜尋能夠找到值,您可以檢查結果中返回的條目數量。
查詢 API,例如 find(),返回型別為 Query 的變數。您可以使用查詢物件在執行 exec() 方法之前分部分構建查詢。exec() 執行查詢並返回一個 Promise,您可以 await 該 Promise 以獲取結果。
// find all athletes that play tennis
const query = Athlete.find({ sport: "Tennis" });
// selecting the 'name' and 'age' fields
query.select("name age");
// limit our results to 5 items
query.limit(5);
// sort by age
query.sort({ age: -1 });
// execute the query at a later time
query.exec();
上面我們已經在 find() 方法中定義了查詢條件。我們也可以使用 where() 函式來實現這一點,並且我們可以使用點運算子 (.) 將查詢的所有部分連結在一起,而不是單獨新增它們。下面的程式碼片段與我們上面的查詢相同,並額外添加了年齡條件。
Athlete.find()
.where("sport")
.equals("Tennis")
.where("age")
.gt(17)
.lt(50) // Additional where query
.limit(5)
.sort({ age: -1 })
.select("name age")
.exec();
find() 方法獲取所有匹配的記錄,但通常您只想獲取一個匹配。以下方法查詢單個記錄
findById():查詢具有指定id的文件(每個文件都有唯一的id)。findOne():查詢匹配指定條件的單個文件。findByIdAndDelete()、findByIdAndUpdate()、findOneAndRemove()、findOneAndUpdate():透過id或條件查詢單個文件,並對其進行更新或刪除。這些是用於更新和刪除記錄的便捷函式。
注意: 還有一個 countDocuments() 方法,您可以使用它來獲取匹配條件的項數。如果您想在不實際獲取記錄的情況下執行計數,這很有用。
查詢還有很多其他功能。有關更多資訊,請參閱:查詢 (Mongoose 文件)。
使用相關文件 — 填充
您可以使用 ObjectId schema 欄位從一個文件/模型例項建立對另一個文件/模型例項的引用,或使用 ObjectId 陣列從一個文件建立對多個文件的引用。該欄位儲存相關模型的 ID。如果需要關聯文件的實際內容,可以在查詢中使用 populate() 方法將 ID 替換為實際資料。
例如,下面的 schema 定義了作者和故事。每個作者可以有多個故事,我們將其表示為 ObjectId 陣列。每個故事可以有一個作者。ref 屬性告訴 schema 哪個模型可以分配給此欄位。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const authorSchema = new Schema({
name: String,
stories: [{ type: Schema.Types.ObjectId, ref: "Story" }],
});
const storySchema = new Schema({
author: { type: Schema.Types.ObjectId, ref: "Author" },
title: String,
});
const Story = mongoose.model("Story", storySchema);
const Author = mongoose.model("Author", authorSchema);
我們可以透過分配 _id 值來儲存對相關文件的引用。下面我們建立一個作者,然後是一個故事,並將作者 ID 分配給故事的作者欄位。
const bob = new Author({ name: "Bob Smith" });
await bob.save();
// Bob now exists, so lets create a story
const story = new Story({
title: "Bob goes sledding",
author: bob._id, // assign the _id from our author Bob. This ID is created by default!
});
await story.save();
注意: 這種程式設計風格的一個巨大好處是,我們不必用錯誤檢查來使程式碼的主路徑複雜化。如果任何 save() 操作失敗,Promise 將被拒絕並丟擲錯誤。我們的錯誤處理程式碼單獨處理這個問題(通常在 catch() 塊中),因此我們程式碼的意圖非常清晰。
我們的故事文件現在有一個由作者文件 ID 引用的作者。為了在故事結果中獲取作者資訊,我們使用 populate(),如下所示。
Story.findOne({ title: "Bob goes sledding" })
.populate("author") // Replace the author id with actual author information in results
.exec();
注意: 敏銳的讀者會注意到我們給故事添加了一位作者,但我們沒有做任何事情將我們的故事新增到作者的 stories 陣列中。那麼我們如何才能獲取特定作者的所有故事呢?一種方法是將我們的故事新增到 stories 陣列中,但這會導致我們在兩個地方維護作者和故事相關資訊。
更好的方法是獲取我們作者的 _id,然後使用 find() 在所有故事的作者欄位中搜索它。
Story.find({ author: bob._id }).exec();
這幾乎是您在本教程中需要了解的有關處理相關專案的所有資訊。有關更詳細的資訊,請參閱 填充 (Mongoose 文件)。
每個檔案一個 schema/模型
雖然您可以使用您喜歡的任何檔案結構建立 schema 和模型,但我們強烈建議在自己的模組(檔案)中定義每個模型 schema,然後匯出建立模型的方法。如下所示
// File: ./models/some-model.js
// Require Mongoose
const mongoose = require("mongoose");
// Define a schema
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date,
});
// Export function to create "SomeModel" model class
module.exports = mongoose.model("SomeModel", SomeModelSchema);
然後,您可以在其他檔案中立即引用並使用該模型。下面我們展示瞭如何使用它來獲取模型的所有例項。
// Create a SomeModel model just by requiring the module
const SomeModel = require("../models/some-model");
// Use the SomeModel object (model) to find all SomeModel records
const modelInstances = await SomeModel.find().exec();
設定 MongoDB 資料庫
現在我們瞭解了 Mongoose 能做什麼以及我們如何設計模型,是時候開始開發 LocalLibrary 網站了。我們首先要做的是設定一個 MongoDB 資料庫,用於儲存我們的圖書館資料。
對於本教程,我們將使用 MongoDB Atlas 雲託管的沙盒資料庫。此資料庫層不適合生產網站,因為它沒有冗餘,但非常適合開發和原型設計。我們在此處使用它,因為它免費且易於設定,並且因為 MongoDB Atlas 是一種流行的資料庫即服務供應商,您可能會合理地選擇它作為生產資料庫(在撰寫本文時,其他流行的選擇包括 ScaleGrid 和 ObjectRocket)。
注意: 如果您願意,可以透過下載並安裝適用於您系統的相應二進位制檔案來在本地設定 MongoDB 資料庫。本文其餘說明將類似,只是連線時指定的資料庫 URL 會有所不同。在Express 教程第 7 部分:部署到生產環境教程中,我們同時將應用程式和資料庫託管在Railway上,但我們同樣可以使用MongoDB Atlas上的資料庫。
您首先需要建立一個 MongoDB Atlas 賬戶(這是免費的,只需您填寫基本的聯絡方式並同意其服務條款)。
登入後,您將被帶到主頁
-
單擊概覽部分的+ 建立按鈕。

-
這將開啟部署叢集螢幕。單擊M0 FREE選項模板。

-
向下滾動頁面以檢視您可以選擇的不同選項。

- 您可以在叢集名稱下更改叢集的名稱。在本教程中,我們將其保留為
Cluster0。 - 取消選擇預載入示例資料集複選框,因為我們稍後會匯入自己的示例資料。
- 從提供商和區域部分選擇任何提供商和區域。不同的區域提供不同的提供商。
- 標籤是可選的。我們在這裡不使用它們。
- 單擊建立部署按鈕(叢集建立需要幾分鐘)。
- 您可以在叢集名稱下更改叢集的名稱。在本教程中,我們將其保留為
-
這將開啟安全快速入門部分。

-
輸入您的應用程式用於訪問資料庫的使用者名稱和密碼(上面我們建立了一個新登入名“cooluser”)。請記住安全地複製和儲存憑據,因為我們稍後會用到它們。單擊建立使用者按鈕。
注意: 避免在 MongoDB 使用者密碼中使用特殊字元,因為 mongoose 可能無法正確解析連線字串。
-
選擇按當前 IP 地址新增以允許從您當前的計算機訪問
-
在 IP 地址欄位中輸入
0.0.0.0/0,然後單擊新增條目按鈕。這告訴 MongoDB 我們希望允許從任何地方訪問。注意: 最佳實踐是限制可以連線到資料庫和其他資源的 IP 地址。在這裡,我們允許從任何地方連線,因為我們不知道部署後請求將來自何處。
-
單擊完成並關閉按鈕。
-
-
這將開啟以下螢幕。單擊轉到概覽按鈕。

-
您將返回到概覽螢幕。單擊左側部署選單下的資料庫部分。單擊瀏覽集合按鈕。

-
這將開啟集合部分。單擊新增我自己的資料按鈕。

-
這將開啟建立資料庫螢幕。

- 將新資料庫的名稱輸入為
local_library。 - 將集合名稱輸入為
Collection0。 - 單擊建立按鈕以建立資料庫。
- 將新資料庫的名稱輸入為
-
您將返回到集合螢幕,並已建立了資料庫。

- 單擊概覽選項卡返回到叢集概覽。
-
在 Cluster0 的概覽螢幕上,單擊連線按鈕。

-
這將開啟連線到 Cluster0螢幕。

- 選擇您的資料庫使用者。
- 選擇驅動程式類別,然後選擇驅動程式 Node.js 和版本,如圖所示。
- 請勿安裝建議的驅動程式。
- 單擊複製圖示以複製連線字串。
- 將其貼上到您的本地文字編輯器中。
- 將連線字串中的
<password>佔位符替換為您的使用者密碼。 - 在選項之前,將資料庫名稱“local_library”插入到路徑中(
...mongodb.net/local_library?retryWrites...) - 將包含此字串的檔案安全地儲存起來。
您現在已經建立了資料庫,並擁有一個可用於訪問它的 URL(包含使用者名稱和密碼)。它將類似於:mongodb+srv://your_user_name:your_password@cluster0.cojoign.mongodb.net/local_library?retryWrites=true&w=majority&appName=Cluster0
安裝 Mongoose
開啟命令提示符並導航到您建立骨架本地圖書館網站的目錄。輸入以下命令以安裝 Mongoose(及其依賴項)並將其新增到您的package.json檔案中,除非您在閱讀上面的Mongoose 入門時已經完成。
npm install mongoose
連線到 MongoDB
開啟 bin/www(從您的專案根目錄)並將以下文字複製到您設定埠的位置(在 app.set("port", port); 行之後)。將資料庫 URL 字串(“insert_your_database_url_here”)替換為代表您自己資料庫的位置 URL(即,使用來自 MongoDB Atlas 的資訊)。
// Set up mongoose connection
const mongoose = require("mongoose");
mongoose.set("strictQuery", false);
const mongoDB = "insert_your_database_url_here";
async function connectMongoose() {
await mongoose.connect(mongoDB);
}
try {
connectMongoose();
} catch (err) {
console.error("Failed to connect to MongoDB:", err);
process.exit(1);
}
如上面 Mongoose 簡介 中所討論,此程式碼建立到資料庫的預設連線並將任何錯誤報告到控制檯。
注意: 我們本可以將資料庫連線程式碼放在 app.js 程式碼中。將其放在應用程式入口點解耦了應用程式和資料庫,這使得為執行測試程式碼使用不同的資料庫變得更容易。
請注意,不建議像上面所示那樣將資料庫憑據硬編碼到原始碼中。我們在這裡這樣做是因為它顯示了核心連線程式碼,並且在開發過程中,洩露這些詳細資訊不會暴露或損壞敏感資訊,風險不大。我們將在部署到生產環境時向您展示如何更安全地執行此操作!
定義 LocalLibrary Schema
我們將為每個模型定義一個單獨的模組,如上文所述。首先在專案根目錄 (/models) 中建立一個用於存放模型的資料夾,然後為每個模型建立單獨的檔案
/express-locallibrary-tutorial # the project root
/models
author.js
book.js
bookinstance.js
genre.js
作者模型
複製下面顯示的 Author schema 程式碼並將其貼上到您的 ./models/author.js 檔案中。該 schema 定義作者具有名字和姓氏的 String SchemaType(必填,最大長度為 100 個字元),以及出生日期和死亡日期的 Date 欄位。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const AuthorSchema = new Schema({
first_name: { type: String, required: true, maxLength: 100 },
family_name: { type: String, required: true, maxLength: 100 },
date_of_birth: { type: Date },
date_of_death: { type: Date },
});
// Virtual for author's full name
AuthorSchema.virtual("name").get(function () {
// To avoid errors in cases where an author does not have either a family name or first name
// We want to make sure we handle the exception by returning an empty string for that case
let fullname = "";
if (this.first_name && this.family_name) {
fullname = `${this.family_name}, ${this.first_name}`;
}
return fullname;
});
// Virtual for author's URL
AuthorSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/catalog/author/${this._id}`;
});
// Export model
module.exports = mongoose.model("Author", AuthorSchema);
我們還為 AuthorSchema 聲明瞭一個名為“url”的虛擬屬性,它返回獲取特定模型例項所需的絕對 URL——我們將在模板中需要獲取特定作者連結時使用該屬性。
注意: 將我們的 URL 宣告為 schema 中的虛擬屬性是一個好主意,因為這樣專案的 URL 只需在一個地方更改。此時,使用此 URL 的連結將無法工作,因為我們還沒有任何處理單個模型例項的路由程式碼。我們將在後面的文章中設定這些!
在模組的末尾,我們匯出模型。
書籍模型
最後,複製下面顯示的 Book schema 程式碼並將其貼上到您的 ./models/book.js 檔案中。大部分與作者模型類似——我們聲明瞭一個包含多個字串欄位的 schema 和一個用於獲取特定書籍記錄 URL 的虛擬屬性,並且我們匯出了模型。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const BookSchema = new Schema({
title: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: "Author", required: true },
summary: { type: String, required: true },
isbn: { type: String, required: true },
genre: [{ type: Schema.Types.ObjectId, ref: "Genre" }],
});
// Virtual for book's URL
BookSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/catalog/book/${this._id}`;
});
// Export model
module.exports = mongoose.model("Book", BookSchema);
這裡的主要區別在於我們建立了兩個對其他模型的引用
- 作者是對單個
Author模型物件的引用,並且是必需的。 - 型別是對
Genre模型物件陣列的引用。我們尚未宣告此物件!
BookInstance 模型
最後,複製下面顯示的 BookInstance schema 程式碼並將其貼上到您的 ./models/bookinstance.js 檔案中。BookInstance 代表一本書的特定副本,某人可能會借閱,幷包含有關副本是否可用、預期歸還日期和“印記”(或版本)詳細資訊的資訊。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const BookInstanceSchema = new Schema({
book: { type: Schema.Types.ObjectId, ref: "Book", required: true }, // reference to the associated book
imprint: { type: String, required: true },
status: {
type: String,
required: true,
enum: ["Available", "Maintenance", "Loaned", "Reserved"],
default: "Maintenance",
},
due_back: { type: Date, default: Date.now },
});
// Virtual for bookinstance's URL
BookInstanceSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/catalog/bookinstance/${this._id}`;
});
// Export model
module.exports = mongoose.model("BookInstance", BookInstanceSchema);
我們在這裡展示的新內容是欄位選項
enum:這允許我們設定字串的允許值。在這種情況下,我們用它來指定我們書籍的可用性狀態(使用 enum 意味著我們可以防止狀態出現拼寫錯誤和任意值)。default:我們使用 default 將新建立的書籍例項的預設狀態設定為“Maintenance”,將預設的due_back日期設定為now(請注意如何設定日期時呼叫 Date 函式!)。
其他一切都應與我們以前的 schema 相同。
型別模型 - 挑戰
開啟您的 ./models/genre.js 檔案並建立一個 schema,用於儲存型別(書籍的類別,例如,它是小說還是非小說,浪漫還是軍事歷史等)。
該定義將與其它模型非常相似
- 該模型應具有一個名為
name的StringSchemaType,用於描述型別。 - 此名稱應為必填項,字元長度在 3 到 100 之間。
- 宣告一個名為
url的型別 URL 的虛擬屬性。 - 匯出模型。
測試 — 建立一些專案
就這樣。我們現在已經設定了網站的所有模型!
為了測試模型(並建立一些我們可以在下一篇文章中使用的示例書籍和其他專案),我們現在將執行一個獨立指令碼來建立每種型別的專案
-
將檔案 populatedb.js 下載(或以其他方式建立)到您的 express-locallibrary-tutorial 目錄中(與
package.json位於同一級別)。注意:
populatedb.js中的程式碼可能有助於學習 JavaScript,但理解它對於本教程來說不是必需的。 -
在命令提示符下使用 node 執行指令碼,傳入您的 MongoDB 資料庫的 URL(與您之前在
app.js中替換 insert_your_database_url_here 佔位符的 URL 相同)bashnode populatedb <your MongoDB url>注意: 在 Windows 上,您需要將資料庫 URL 用雙引號 (") 括起來。在其他作業系統上,您可能需要單引號 (')。
-
指令碼應執行完成,並在終端中顯示它建立的專案。
注意: 轉到您在 MongoDB Atlas 上的資料庫(在集合選項卡中)。您現在應該能夠深入到書籍、作者、型別和書籍例項的各個集合中,並檢視單個文件。
總結
在本文中,我們學習了一些關於 Node/Express 上的資料庫和 ORM 的知識,以及很多關於 Mongoose schema 和模型如何定義的知識。然後,我們使用這些資訊為 LocalLibrary 網站設計和實現了 Book、BookInstance、Author 和 Genre 模型。
最後,我們透過建立多個例項(使用獨立指令碼)測試了我們的模型。在下一篇文章中,我們將探討建立一些頁面來顯示這些物件。
另見
- 資料庫整合 (Express docs)
- Mongoose 網站 (Mongoose docs)
- Mongoose 指南 (Mongoose docs)
- 驗證 (Mongoose docs)
- Schema 型別 (Mongoose docs)
- 模型 (Mongoose docs)
- 查詢 (Mongoose docs)
- 填充 (Mongoose docs)