Express 教程第 3 部分:使用資料庫(使用 Mongoose)
本文簡要介紹了資料庫以及如何在 Node/Express 應用程式中使用它們。然後繼續展示如何使用 Mongoose 為 LocalLibrary 網站提供資料庫訪問。它解釋瞭如何宣告物件模式和模型、主要欄位型別以及基本驗證。它還簡要展示了一些訪問模型資料的主要方法。
| 先決條件 | Express 教程第 2 部分:建立網站框架 |
|---|---|
| 目標 | 能夠使用 Mongoose 設計和建立自己的模型。 |
概述
圖書館工作人員將使用 Local Library 網站儲存有關書籍和借閱者的資訊,而圖書館成員將使用它來瀏覽和搜尋書籍,瞭解是否有任何副本可用,然後預訂或借閱它們。為了有效地儲存和檢索資訊,我們將把它儲存在資料庫中。
Express 應用程式可以使用許多不同的資料庫,並且有幾種方法可以用於執行Create、Read、Update 和Delete (CRUD) 操作。本教程簡要概述了一些可用的選項,然後詳細介紹了所選的特定機制。
我可以用哪些資料庫?
與資料庫互動的最佳方法是什麼?
有兩種常見的方法可以與資料庫互動
- 使用資料庫的原生查詢語言,例如 SQL。
- 使用物件關係對映器 (“ORM”)。ORM 將網站的資料表示為 JavaScript 物件,然後將其對映到底層資料庫。一些 ORM 與特定資料庫繫結,而另一些則提供資料庫無關的後端。
透過使用 SQL 或資料庫支援的任何查詢語言,可以獲得最佳的效能。ODM 通常較慢,因為它們使用翻譯程式碼在物件和資料庫格式之間進行對映,這可能不會使用最有效的資料庫查詢(如果 ODM 支援不同的資料庫後端,並且必須在支援哪些資料庫功能方面做出更大的折衷,則尤其如此)。
使用 ORM 的好處是程式設計師可以繼續使用 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 作為資料庫,它是一個合理的選擇。
為 LocalLibrary 使用 Mongoose 和 MongoDB
對於Local Library 示例(以及本主題的其餘部分),我們將使用 Mongoose ODM 來訪問我們的圖書館資料。Mongoose 充當 MongoDB 的前端,MongoDB 是一個開源的 NoSQL 資料庫,它使用面向文件的資料模型。MongoDB 資料庫中“文件”的“集合”類似於關係資料庫中“行”的“表”。
這種 ODM 和資料庫組合在 Node 社群中非常流行,部分原因是文件儲存和查詢系統非常類似於 JSON,因此 JavaScript 開發人員很熟悉。
注意:您無需瞭解 MongoDB 即可使用 Mongoose,儘管 Mongoose 文件 的某些部分確實更容易使用和理解,如果您已經熟悉 MongoDB。
本教程的其餘部分將展示如何為 LocalLibrary 網站 示例定義和訪問 Mongoose 模式和模型。
設計 LocalLibrary 模型
在您開始編寫模型程式碼之前,花幾分鐘時間考慮一下我們需要儲存哪些資料以及不同物件之間的關係是值得的。
我們知道我們需要儲存有關書籍的資訊(標題、摘要、作者、流派、ISBN),並且我們可能有多個副本可用(具有全域性唯一的 ID、可用性狀態等)。我們可能需要儲存有關作者的資訊,而不僅僅是他們的姓名,並且可能有多個作者具有相同或相似的姓名。我們希望能夠根據書名、作者、流派和類別對資訊進行排序。
在設計模型時,為每個“物件”(一組相關資訊)建立單獨的模型是有意義的。在這種情況下,這些模型的一些明顯候選者是書籍、書籍例項和作者。
您可能還想使用模型來表示選擇列表選項(例如,像一個下拉列表中的選項),而不是將選項硬編碼到網站本身中——當並非所有選項都預先知道或可能發生更改時,建議這樣做。一個很好的例子是流派(例如,奇幻、科幻等)。
一旦我們確定了模型和欄位,我們就需要考慮它們之間的關係。
考慮到這一點,下面的 UML 關聯圖顯示了我們將在此處定義的模型(作為框)。如上所述,我們為書籍(書籍的通用詳細資訊)、書籍例項(系統中可用書籍的特定物理副本的狀態)和作者建立了模型。我們還決定為流派建立一個模型,以便可以動態建立值。我們決定不為BookInstance:status建立模型——我們將硬編碼可接受的值,因為我們預計這些值不會改變。在每個框內,您可以看到模型名稱、欄位名稱和型別,以及方法及其返回型別。
該圖還顯示了模型之間的關係,包括它們的多重性。多重性是圖中顯示的數字,表示關係中可能存在的每個模型的數量(最大值和最小值)。例如,框之間的連線線顯示Book和Genre是相關的。Book模型附近的數字顯示Genre必須具有零個或多個Book(任意多個),而線上另一端靠近Genre的數字顯示一本Book可以具有零個或多個關聯的Genre。
注意:如我們在下面的 Mongoose 入門 中所述,最好在一個模型中擁有定義文件/模型之間關係的欄位(您仍然可以透過在另一個模型中搜索關聯的_id來查詢反向關係)。在下面,我們選擇在 Book 模式中定義Book/Genre和Book/Author之間的關係,以及Book/BookInstance在BookInstance模式中的關係。這個選擇在某種程度上是任意的——我們同樣可以在另一個模式中擁有該欄位。
注意:下一節提供了一個基本入門指南,解釋瞭如何定義和使用模型。在閱讀時,請考慮我們將如何構建上圖中的每個模型。
資料庫 API 是非同步的
建立、查詢、更新或刪除記錄的資料庫方法是非同步的。這意味著這些方法會立即返回,並且處理方法成功或失敗的程式碼會在操作完成後稍後執行。在伺服器等待資料庫操作完成的同時,其他程式碼可以執行,因此伺服器可以保持對其他請求的響應。
JavaScript 有許多機制來支援非同步行為。歷史上,JavaScript 很大程度上依賴於將 回撥函式 傳遞給非同步方法來處理成功和錯誤情況。在現代 JavaScript 中,回撥已被 Promise 大量取代。Promise 是非同步方法(立即)返回的物件,表示其未來的狀態。當操作完成後,promise 物件會“完成”,並解析一個表示操作結果或錯誤的物件。
有兩種主要方法可以使用 promise 在 promise 完成時執行程式碼,我們強烈建議您閱讀 如何使用 promise 以獲取這兩種方法的高階概述。在本教程中,我們將主要使用 await 在 async function 中等待 promise 完成,因為這會導致更易讀和易懂的非同步程式碼。
這種方法的工作原理是,使用async function關鍵字將函式標記為非同步函式,然後在該函式內部對返回 Promise 的任何方法應用await。當非同步函式執行時,它的操作會在第一個await方法處暫停,直到 Promise 完成。從周圍程式碼的角度來看,非同步函式隨後返回,並且其後的程式碼能夠執行。稍後,當 Promise 完成時,非同步函式內部的await方法會返回結果,或者如果 Promise 被拒絕則丟擲錯誤。然後非同步函式中的程式碼繼續執行,直到遇到另一個await(此時它將再次暫停),或者直到函式中的所有程式碼都已執行。
您可以在下面的示例中看到它是如何工作的。myFunction()是一個非同步函式,它在try...catch塊內呼叫。當myFunction()執行時,程式碼執行在methodThatReturnsPromise()處暫停,直到 Promise 解析,此時程式碼繼續執行到aFunctionThatReturnsPromise()並再次等待。如果非同步函式中丟擲錯誤,則catch塊中的程式碼將執行,如果任一方法返回的 Promise 被拒絕,則會發生這種情況。
async function myFunction {
// ...
await someObject.methodThatReturnsPromise();
// ...
await aFunctionThatReturnsPromise();
// ...
}
try {
// ...
myFunction();
// ...
} catch (e) {
// error handling code
}
上面的非同步方法按順序執行。如果這些方法彼此不依賴,則可以並行執行它們,並更快地完成整個操作。這是使用Promise.all()方法完成的,該方法以 Promise 的可迭代物件作為輸入並返回單個Promise。當所有輸入的 Promise 都完成時,此返回的 Promise 就會完成,幷包含一個包含完成值的陣列。當任何輸入的 Promise 被拒絕時,它就會被拒絕,幷包含第一個拒絕原因。
下面的程式碼展示了它是如何工作的。首先,我們有兩個返回 Promise 的函式。我們使用Promise.all()返回的 Promise 對它們都執行await以完成。一旦它們都完成,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 資料庫,如何定義模式和模型,以及如何執行基本查詢。
注意:本入門指南深受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 是非同步的部分所述,這裡我們在非同步函式內對connect()方法返回的 Promise 執行await。我們使用 Promise 的catch()處理程式來處理連線嘗試時發生的任何錯誤,但我們也可以在try...catch塊中呼叫main()。
您可以使用mongoose.connection獲取預設的Connection物件。如果您需要建立其他連線,可以使用mongoose.createConnection()。這採用與connect()相同的資料庫 URI 格式(包含主機、資料庫、埠、選項等),並返回一個Connection物件)。請注意,createConnection()會立即返回;如果您需要等待連線建立,則可以將其與asPromise()一起呼叫以返回一個 Promise(mongoose.createConnection(mongoDB).asPromise())。
定義和建立模型
模型使用Schema介面定義。模式允許您定義儲存在每個文件中的欄位以及它們的驗證要求和預設值。此外,您可以定義靜態和例項幫助器方法,以便更容易地處理您的資料型別,以及您可以像使用任何其他欄位一樣使用的虛擬屬性,但這些屬性實際上並未儲存在資料庫中(我們將在下面進一步討論)。
然後,模式使用mongoose.model()方法“編譯”成模型。擁有模型後,您可以使用它來查詢、建立、更新和刪除給定型別的物件。
注意:每個模型都對映到 MongoDB 資料庫中的文件的集合。這些文件將包含在模型Schema中定義的欄位/模式型別。
下面的程式碼片段顯示瞭如何定義一個簡單的模式。首先,您require() mongoose,然後使用 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()方法從模式建立。
// 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建立資料庫集合),第二個引數是要用於建立模型的模式。
注意:定義完模型類後,您可以使用它們來建立、更新或刪除記錄,並執行查詢以獲取所有記錄或特定記錄子集。我們將在使用模型部分以及建立檢視時向您展示如何執行此操作。
模式型別(欄位)
一個模式可以有任意數量的欄位——每個欄位都代表儲存在MongoDB中的文件中的一個欄位。下面顯示了一個示例模式,其中顯示了許多常見的欄位型別以及它們是如何宣告的。
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:任意模式型別。[]:專案的陣列。您可以對這些模型執行 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 文件)。
方法和查詢幫助器
模式還可以具有例項方法、靜態方法和查詢幫助器。例項方法和靜態方法類似,但顯而易見的區別在於例項方法與特定記錄相關聯,並且可以訪問當前物件。查詢幫助器允許您擴充套件 mongoose 的可連結查詢構建器 API(例如,允許您除了find()、findOne()和findById()方法之外再新增一個“byName”查詢)。
使用模型
建立模式後,您可以使用它來建立模型。模型表示資料庫中您可以搜尋的文件集合,而模型的例項表示您可以儲存和檢索的單個文件。
下面我們將簡要概述。有關更多資訊,請參閱:模型(Mongoose 文件)。
注意:記錄的建立、更新、刪除和查詢都是非同步操作,它們返回一個Promise。下面的示例僅顯示了相關方法和await的使用(即使用這些方法的基本程式碼)。為了清晰起見,省略了周圍的async function和try...catch塊以捕獲錯誤。有關使用await/async的更多資訊,請參閱上面資料庫 API 是非同步的。
建立和修改文件
要建立記錄,您可以定義模型的例項,然後在其上呼叫save()。下面的示例假設SomeModel是我們從模式建立的模型(具有單個欄位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以獲取結果。
// 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模式欄位從一個文件/模型例項引用另一個文件/模型例項,或者使用ObjectId陣列從一個文件引用多個文件。該欄位儲存相關模型的 ID。如果您需要關聯文件的實際內容,則可以在查詢中使用populate()方法將 ID 替換為實際資料。
例如,以下模式定義了作者和故事。每個作者可以有多個故事,我們將其表示為ObjectId陣列。每個故事可以有一個作者。ref屬性告訴模式可以為該欄位分配哪個模型。
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 文件)。
每個檔案一個模式/模型
雖然您可以使用任何您喜歡的檔案結構建立模式和模型,但我們強烈建議在每個模組(檔案)中定義每個模型模式,然後匯出建立模型的方法。如下所示
// File: ./models/somemodel.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/somemodel");
// 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 免費選項模板。

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

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

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

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

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

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

- 將新資料庫的名稱輸入為
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
開啟/app.js(在專案的根目錄中)並在宣告Express 應用程式物件的下方複製以下文字(在const app = express();行之後)。將資料庫 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";
main().catch((err) => console.log(err));
async function main() {
await mongoose.connect(mongoDB);
}
如上面Mongoose 入門指南中所述,此程式碼建立到資料庫的預設連線,並將任何錯誤報告到控制檯。
請注意,如上所示在原始碼中硬編碼資料庫憑據是不推薦的。我們在這裡這樣做是因為它顯示了核心連線程式碼,並且因為在開發過程中,洩露這些詳細資訊不會造成暴露或破壞敏感資訊的重大風險。我們將在部署到生產環境時向您展示如何更安全地執行此操作!
定義 LocalLibrary 架構
我們將為每個模型定義一個單獨的模組,如上面所述。首先在專案根目錄中建立模型資料夾(/models),然後為每個模型建立單獨的檔案
/express-locallibrary-tutorial // the project root
/models
author.js
book.js
bookinstance.js
genre.js
作者模型
複製下面顯示的Author模式程式碼,並將其貼上到您的./models/author.js檔案中。該模式將作者定義為具有String SchemaTypes 的姓氏和名字(必填,最多 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 宣告為虛擬屬性是一個好主意,因為這樣,專案的 URL 只需要在一個地方更改。此時,使用此 URL 的連結將無法工作,因為我們還沒有任何處理單個模型例項的路由程式碼。我們將在後面的文章中設定這些內容!
在模組的末尾,我們匯出模型。
書籍模型
複製下面顯示的Book模式程式碼,並將其貼上到您的./models/book.js檔案中。大部分內容與作者模型類似——我們聲明瞭一個具有多個字串欄位的模式,以及一個用於獲取特定書籍記錄的 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 是對單個
Author模型物件的引用,並且是必需的。 - genre 是對
Genre模型物件陣列的引用。我們還沒有宣告此物件!
BookInstance 模型
最後,複製下面顯示的BookInstance模式程式碼,並將其貼上到您的./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:這允許我們設定字串的允許值。在這種情況下,我們使用它來指定書籍的可用狀態(使用列舉意味著我們可以防止拼寫錯誤和狀態的任意值)。default:我們使用 default 將新建立的書籍例項的預設狀態設定為“維護”,並將預設的due_back日期設定為now(注意如何在設定日期時呼叫 Date 函式!)。
其他所有內容都應該來自我們之前的模式。
流派模型 - 挑戰
開啟您的./models/genre.js檔案,並建立一個用於儲存流派(書籍的類別,例如它是小說還是非小說、浪漫還是軍事歷史等)的模式。
定義將與其他模型非常相似
- 該模型應該有一個名為
name的StringSchemaType 來描述流派。 - 此名稱應該是必需的,並且必須在 3 到 100 個字元之間。
- 為流派的 URL 宣告一個名為
url的虛擬屬性。 - 匯出模型。
測試 - 建立一些專案
就是這樣。我們現在已經設定了網站的所有模型!
為了測試模型(並建立一些示例書籍和其他專案,以便我們可以在下一篇文章中使用),我們現在將執行一個獨立指令碼以建立每種型別的專案
- 在您的express-locallibrary-tutorial目錄中下載(或以其他方式建立)檔案populatedb.js(與
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 模式和模型的許多知識。然後,我們使用此資訊為本地圖書館網站設計並實現了Book、BookInstance、Author和Genre模型。
最後,我們透過建立多個例項(使用獨立指令碼)來測試我們的模型。在下一篇文章中,我們將介紹如何建立一些頁面來顯示這些物件。
另請參閱
- 資料庫整合(Express 文件)
- Mongoose 網站(Mongoose 文件)
- Mongoose 指南(Mongoose 文件)
- 驗證(Mongoose 文件)
- 模式型別(Mongoose 文件)
- 模型(Mongoose 文件)
- 查詢(Mongoose 文件)
- 填充(Mongoose 文件)