Mongoose 與 Node.js - 對像數據建模
簡介
NoSQL 為數據庫的表格世界帶來了靈活性。 MongoDB 尤其成為存儲非結構化 JSON 文檔的絕佳選擇。數據在 UI 中以 JSON 形式開始,經過很少的轉換才能存儲,因此我們可以從提高性能和減少處理時間中獲益。
但是 NoSQL 並不意味著完全沒有結構。我們仍然需要在存儲數據之前驗證和轉換我們的數據,並且我們仍然可能需要對其應用一些業務邏輯。那是貓鼬填補的地方。
在本文中,我們將通過一個示例應用程序了解如何使用 Mongoose 對數據進行建模並在將其存儲到 MongoDB 之前對其進行驗證。
我們將為一個家譜應用程序編寫模型,一個具有一些個人屬性的人,包括他們的父母是誰。我們還將了解如何使用此模型創建和修改 Persons 並將它們保存到 MongoDB。
什麼是貓鼬?
MongoDB 的工作原理
要了解什麼是 Mongoose,我們首先需要大致了解 MongoDB 的工作原理。我們可以在 MongoDB 中保存的基本數據單位是 Document。雖然存儲為二進制,但當我們查詢數據庫時,我們會獲得它作為 JSON 對象的表示形式。
相關文檔可以存儲在集合中,類似於關係數據庫中的表。類比到此結束,因為我們定義了要考慮的“相關文檔”。
MongoDB 不會對文檔強制執行結構。例如,我們可以將此文檔保存到 Person
收藏:
{
"name": "Alice"
}
然後在同一個集合中,我們可以保存一個沒有共享屬性或結構的看似無關的文檔:
{
"latitude": 53.3498,
"longitude": 6.2603
}
這就是 NoSQL 數據庫的新穎之處。我們為我們的數據創造意義,並以我們認為最好的方式存儲它。數據庫不會施加任何限制。
貓鼬目的
儘管 MongoDB 不會強加一種結構,但應用程序通常使用一個結構來管理數據。我們收到數據並需要對其進行驗證,以確保我們收到的是我們需要的。我們可能還需要在保存數據之前以某種方式處理數據。這就是 Mongoose 發揮作用的地方。
Mongoose 是一個用於 NodeJS 應用程序的 NPM 包。它允許為我們的數據定義模式以適應,同時還抽象了對 MongoDB 的訪問。這樣我們可以確保所有保存的文檔共享一個結構並包含所需的屬性。
現在讓我們看看如何定義一個 schema .
安裝 Mongoose 並創建 Person Schema
讓我們使用默認屬性和 person 模式 啟動一個 Node 項目 :
$ npm init -y
隨著項目的初始化,讓我們繼續安裝mongoose
使用 npm
:
$ npm install --save mongoose
mongoose
將自動包含 mongodb
NPM 模塊也是如此。你自己不會直接使用它。由貓鼬處理。
要使用 Mongoose,我們需要將其導入到我們的腳本中:
let mongoose = require('mongoose');
然後連接到數據庫:
mongoose.connect('mongodb://localhost:27017/genealogy', {useNewUrlParser: true, useUnifiedTopology: true});
由於數據庫尚不存在,因此將創建一個。我們將使用最新的工具來解析連接字符串,通過設置 useNewUrlParser
到 true
我們還將使用帶有 useUnifiedTopology
的最新 MongoDB 驅動程序 作為 true
.
mongoose.connect()
假設 MongoDB 服務器在默認端口上本地運行並且沒有憑據。讓 MongoDB 以這種方式運行的一種簡單方法是 Docker:
$ docker run -p 27017:27017 mongo
創建的容器足以讓我們嘗試 Mongoose,雖然保存到 MongoDB 的數據不會持久化。
人員架構和模型
在前面必要的解釋之後,我們現在可以專注於編寫我們的person schema 並從中編譯模型。
Mongoose 中的模式映射到 MongoDB 集合併定義該集合中所有文檔的格式。架構內的所有屬性都必須有一個分配的 SchemaType
.比如我們的Person
的名字 可以這樣定義:
const PersonSchema = new mongoose.Schema({
name: { type: String},
});
或者更簡單,像這樣:
const PersonSchema = new mongoose.Schema({
name: String,
});
String
是幾個 SchemaTypes
之一 由貓鼬定義。您可以在 Mongoose 文檔中找到其餘部分。
對其他模式的引用
我們可以預期,所有中型應用程序都會有不止一個模式,並且這些模式可能會以某種方式鏈接。
在我們的示例中,為了表示一個家譜,我們需要向我們的模式添加兩個屬性:
const PersonSchema = new mongoose.Schema({
// ...
mother: { type: mongoose.Schema.Types.ObjectId, ref: 'Person' },
father: { type: mongoose.Schema.Types.ObjectId, ref: 'Person' },
});
一個人可以有一個mother
和一個 father
.在 Mongoose 中表示這一點的方法是保存引用文檔的 ID,mongoose.Schema.Types.ObjectId
,而不是對象本身。
ref
property 必須是我們引用的模型的名稱。我們稍後會看到更多關於模型的信息,但現在知道一個模式只與一個模型相關就足夠了,並且 'Person'
是PersonSchema
的型號 .
我們的案例有點特殊,因為 mother
和 father
也將包含人,但定義這些關係的方式在所有情況下都是相同的。
內置驗證
所有 SchemaType
s 帶有默認的內置驗證。我們可以根據所選的 SchemaType
定義限制和其他要求 .要查看一些示例,讓我們添加一個 surname
, yearBorn
, 和 notes
到我們的 Person
:
const PersonSchema = new mongoose.Schema({
name: { type: String, index: true, required: true },
surname: { type: String, index: true },
yearBorn: { type: Number, min: -5000, max: (new Date).getFullYear() },
notes: { type: String, minlength: 5 },
});
全部內置SchemaType
s 可以是 required
.在我們的例子中,我們希望所有人至少有一個名字。 Number
type 允許設置最小值和最大值,甚至可以計算。
index
屬性將使 Mongoose 在數據庫中創建索引。這有助於查詢的有效執行。上面,我們定義了人的name
和 surname
成為索引。我們將始終按姓名搜索人。
自定義驗證
內置 SchemaType
s 允許定制。當我們有一個只能保存某些值的屬性時,這特別有用。讓我們添加 photosURLs
我們的 Person
屬性 , 一組 URLs 他們的照片:
const PersonSchema = new mongoose.Schema({
// ...
photosURLs: [
{
type: String,
validate: {
validator: function(value) {
const urlPattern = /(http|https):\/\/(\w+:{0,1}\w*#)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%#!\-/]))?/;
const urlRegExp = new RegExp(urlPattern);
return value.match(urlRegExp);
},
message: props => `${props.value} is not a valid URL`
}
}
],
});
photosURLs
只是一個字符串數組,photosURLs: [String]
.這個屬性的特別之處在於我們需要自定義驗證來確認添加的值具有 Internet URL 的格式。
validator()
上面的函數使用匹配典型互聯網 URL 的正則表達式,它必須以 http(s)://
開頭 .
如果我們需要更複雜的 SchemaType
我們可以創建自己的,但如果它已經可用,我們最好搜索一下。
例如 mongoose-type-url 包添加了一個自定義的 SchemaType
我們本來可以使用的,mongoose.SchemaTypes.Url
.
虛擬屬性
虛擬是未保存到數據庫的文檔屬性。它們是計算的結果。在我們的示例中,將一個人的全名設置在一個字符串中而不是在 name
中分隔會很有用 和 surname
.
讓我們看看在我們的初始模式定義之後如何實現這一點:
免費電子書:Git Essentials
查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!
PersonSchema.virtual('fullName').
get(function() {
if(this.surname)
return this.name + ' ' + this.surname;
return this.name;
}).
set(function(fullName) {
fullName = fullName.split(' ');
this.name = fullName[0];
this.surname = fullName[1];
});
虛擬屬性 fullName
為了簡單起見,上面做了一些假設:每個人至少有一個名字,或者一個名字和一個姓氏。如果一個人有中間名或組合名或姓氏,我們將面臨問題。所有這些限制都可以在 get()
中修復 和 set()
上面定義的函數。
因為虛擬沒有保存到數據庫中,所以我們不能在數據庫中搜索人員時將它們用作過濾器。在我們的例子中,我們需要使用 name
和 surname
.
中間件
中間件是可以在標準 Mongoose 方法之前或之後執行的函數或鉤子,例如 save()
或 find()
例如。
一個人可以有一個mother
和一個 father
.正如我們之前所說,我們通過將對象的 id 存儲為人的屬性而不是對象本身來保存這種關係。最好用對象本身而不是僅用 ID 填充這兩個屬性。
這可以通過 pre()
來實現 與 findOne()
關聯的函數 貓鼬方法:
PersonSchema.pre('findOne', function(next) {
this.populate('mother').populate('father');
next();
});
上面的函數需要調用作為參數接收的函數,next()
為了繼續處理其他的鉤子。
populate()
是一種 Mongoose 方法,將 ID 替換為它們所代表的對象,我們在僅搜索一個人時使用它來獲取父母。
我們可以將此鉤子添加到其他搜索功能,例如 find()
.如果我們願意,我們甚至可以遞歸地找到父母。但我們應該處理 populate()
小心,因為每次調用都是從數據庫中提取的。
為模式創建模型
為了開始基於我們的 Person 模式創建文檔,最後一步是基於模式編譯模型:
const Person = mongoose.model('Person', PersonSchema);
第一個參數將是我們所指的集合的單數名稱。這是我們賦予 ref
的值 mother
的屬性 和 father
我們人的屬性。第二個參數是 Schema
我們之前定義的。
model()
方法複製我們在模式上定義的所有內容。它還包含我們將用於與數據庫交互的所有 Mongoose 方法。
從現在開始,模型是我們唯一需要的東西。我們甚至可以使用 module.exports
使該人在我們應用的其他模塊中可用:
module.exports.Person = mongoose.model('Person', PersonSchema);
module.exports.db = mongoose;
我們還導出了 mongoose
模塊。我們需要它在應用程序結束之前斷開與數據庫的連接。
我們可以這樣導入模塊:
const {db, Person} = require('./persistence');
如何使用模型
我們在上一節中編譯的模型包含了與數據庫上的集合進行交互所需的所有內容。
現在讓我們看看如何將我們的模型用於所有 CRUD 操作。
創建人員
我們可以通過簡單地創建一個人:
let alice = new Person({name: 'Alice'});
name
是唯一必需的屬性。讓我們創建另一個人,但這次使用虛擬屬性:
let bob = new Person({fullName: 'Bob Brown'});
現在我們有了前兩個人,我們可以創建一個填充了所有屬性的新人,包括父母:
let charles = new Person({
fullName: 'Charles Brown',
photosURLs: ['https://bit.ly/34Kvbsh'],
yearBorn: 1922,
notes: 'Famous blues singer and pianist. Parents not real.',
mother: alice._id,
father: bob._id,
});
最後一個人的所有值都設置為有效值,因為一旦執行此行,驗證就會觸發錯誤。例如,如果我們將第一張照片的 URL 設置為鏈接以外的內容,我們會收到錯誤:
ValidationError: Person validation failed: photosURLs.0: wrong_url is not a valid URL
如前所述,父母是用前兩個人的 ID 完成的,而不是對象。
我們已經創建了三個人,但他們還沒有存儲到數據庫中。接下來讓我們這樣做:
alice.save();
bob.save();
涉及數據庫的操作是異步的。如果我們想等待完成,我們可以使用 async/await:
await charles.save();
現在所有人都保存到了數據庫中,我們可以使用 find()
檢索他們 和 findOne()
方法。
檢索一個或多個人員
Mongoose 中的所有 find 方法都需要一個參數來過濾搜索。讓我們找回我們創建的最後一個人:
let dbCharles = await Person.findOne({name: 'Charles', surname: 'Brown'}).exec();
findOne()
返回一個查詢,所以為了得到一個結果,我們需要用 exec()
執行它 然後用 await
等待結果 .
因為我們在 findOne()
上附加了一個鉤子 填充該人的父母的方法,我們現在可以直接訪問它們:
console.log(dbCharles.mother.fullName);
在我們的例子中,我們知道查詢只會返回一個結果,但即使有多個人匹配過濾器,也只會返回第一個結果。
如果我們使用 find()
,我們可以獲得多個結果 方法:
let all = await Person.find({}).exec();
我們將返回一個可以迭代的數組。
更新人員
如果我們已經有一個人,無論是因為我們剛剛創建它還是檢索它,我們都可以通過以下方式更新和保存更改:
alice.surname = 'Adams';
charles.photosURLs.push('https://bit.ly/2QJCnMV');
await alice.save();
await charles.save();
因為這兩個人都已經存在於數據庫中,Mongoose 將只發送一個更新命令,只改變字段,而不是整個文檔。
刪除人員
與檢索一樣,可以為一個或多個人進行刪除。接下來讓我們這樣做:
await Person.deleteOne({name: 'Alice'});
await Person.deleteMany({}).exec();
執行完這兩個命令後,集合會是空的。
結論
在本文中,我們看到了 Mongoose 如何在我們的 NodeJS 和 MongoDB 項目中非常有用。
在大多數使用 MongoDB 的項目中,我們需要以某種定義的格式存儲數據。很高興知道 Mongoose 提供了一種簡單的方法來建模和驗證數據。
完整的示例項目可以在 GitHub 上找到。