大家好,我是 Yubin
今天來跟大家介紹一個非常熱門的 MongoDB Driver,Mongoose。
Fastify 對於 Database 的支援非常多,官方或社群都有許多套件可以安裝使用。
這邊我們使用跟 JavaScript 互動最友善的 MongoDB 來做為主要的 Database。
然而 JavaScript 的特性是動態型別,意思是我們可以把原本是 number
的變數,再指定 (assign) 為 string
或其他類型的資料。
var a = 1
a = 'one' // ok
作為一個優質的 NodeJS 開發者,我們使用 TypeScript 來進行開發,
TypeScript 是強型別的語言,不允許不同型態的指定。
let a = 1
a = 'one' // error TS2322: Type 'string' is not assignable to type 'number'
透過強型別的特性,可以避免我們對於資料型態的誤用。
但,MongoDB 是 Schema-less 的資料庫,不用是先定義好 Table Schema 就可以操作。
你可以在這份文件中,寫入:
{
name: 'Fat Orange',
weight: 7
}
同時可以在同一個 Collection 中寫入另外一份資料:
{
name: 'Fat Orange',
weight: 'seven'
}
或是
{
name: 'Fat Orange'
}
都可以,不會出錯。
但這樣會讓資料庫的資料太亂,後續如果要用程式操作的話會很麻煩。
(例如要把所有資料的 weight 相加,但有的 weight 是數值、有的 weight 是字串也有人沒定義 weight)
要解決這個問題的辦法是,在每次要把資料寫入 Database 前,都做型態的檢查,確保每個資料都經過驗證,才寫入資料庫。
但想當然,這樣會造成開發者的負擔,讓我們不能準時下班。
這種時候可以使用 Mongoose 來定義 Schema。
我們可以透過 Mongoose 來定義 Schema,也就是某種資料,應該要長怎樣。
安裝 Mongoose:
npm i mongoose
假設我想定義一個 Model 稱為 Cat,他的 Schema 中,有名稱(型態為字串)、有體重(型態為數值):
因為我們使用 TypeScript,所以先訂好 Cat 在程式中的 Type (interface):
interface ICat {
name: string,
weight: number
}
接著開始定義 Mongoose Schema,產生 Mongoose 的 Model。
import { model, Schema } from 'mongoose'
const catSchema: Schema = new Schema(
{
name: {
type: String,
required: true
},
weight: {
type: Number,
required: true
}
}
)
const Cat = model<ICat>('cat', catSchema) // Cat Model
像這樣定義這個 Schema 有什麼欄位,該欄位的 Type 是什麼,以及可以加一些限制參數,如 required
表示必須擁有這個欄位 (預設為 false
,表示 Optional)。
可以參考 Mongoose 官方文件。
定義好 Schema 後,使用 model()
函式產生 model。model()
第一個參數帶入該 model 的名稱,第二個參數帶入 Mongoose 的 Schema。
注意,model() 中有特別宣告了 Icat
這個 Type,
用意是告訴 TypeScript,這個 Model 拿出來的資料型態是 ICat
。
這個 mongoose 的 Model 是非常重要的,裡面封裝了許多可以操作資料庫的方法。
.create()
可以新增資料。
.find()
可以搜尋全部或依條件搜尋資料。
.findById()
.findByIdAndDelete()
.findByIdAndUpdate()
針對特定 id 的資料進行查詢、修改、刪除等。
還有一堆好用的 method。
其他 Mongoose 的 Model API 可以參考官方文件。
我們來實際嘗試一下,如果沒有符合 Schema 的要求會發生什麼事。
try {
const Cat = model('cat', catSchema) // Cat Model
const newCat = await Cat.create({ name: 'Fat Orange' })
console.log(newCat)
} catch (error) {
console.log(error)
}
要注意的是,像程式中的 .create()
這些 Model Function 很多都是回傳 Promise,記得要用 await 或 callback 做處理。(上述程式使用 async/await
style)
執行後印出:
Error: cat validation failed: weight: Path `weight` is required.
at ValidationError.inspect (node_modules/mongoose/lib/error/validation.js:49:26)
// ...
at Object.throw (server.js:14:53) {
errors: {
weight: ValidatorError: Path `weight` is required.
at validate (node_modules/mongoose/lib/schematype.js:1346:13)
at SchemaNumber.SchemaType.doValidate (node_modules/mongoose/lib/schematype.js:1330:7)
at node_modules/mongoose/lib/document.js:2834:18
at processTicksAndRejections (node:internal/process/task_queues:78:11) {
properties: [Object],
kind: 'required',
path: 'weight',
value: undefined,
reason: undefined,
[Symbol(mongoose:validatorError)]: true
}
},
_message: 'cat validation failed'
}
可以明顯看到,mongoose 拋出了錯誤,
指出 cat validation failed: weight: Path 'weight' is required.
。
只要違反 Schema 定義的那些限制,就會拋出錯誤。
這樣一來,我們可以很安心的操作資料庫,因為只要可以順利執行的資料,必定是符合格式的。
我們知道 Fastify route 也有 Validation 機制。
可以確保進來的 Request Payload 符合預期。
本篇文章所描述的 Mongoose Schema,可以確保進入資料庫的資料符合預期。
使用 Typebox,定義 Payload Schema:
import { Type } from '@sinclair/typebox'
const CatPostBodySchema = Type.Object({
name: Type.String(),
weight: Type.Number()
})
定義在 POST /cats
這個 route 上:
interface CreateCatBody {
name: string
weight: number
}
server.post<{ Body: CreateCatBody }>('/cats', { schema: CatPostBodySchema }, async (request, reply) => {
const name = request.body.name
const weight = request.body.weight
try {
const cat = await Cat.create({ name, weight })
return reply.status(201).send({ cat })
} catch (error) {
return reply.status(500).send({ error })
}
})
執行後發送 POST Request。
透過 route 的 Validation,可以確保進來的格式正確。
透過 mongoose model 的 Schema Validation,可以確保進去 database 的格式正確,不用擔心在處理資料的過程把資料弄亂。
如此一來可以更加提升程式品質與工作體驗。
mongoose.connect()
連到某個 MongoDB Serverimport mongoose from 'mongoose'
const connectionString = 'mongodb://localhost:27017/my-fastify'
await mongoose.connect(connectionString)
也可以寫成 callback style
mongoose.connect(connectionString, (error) => {
if (error) {
console.log(error)
}
})
MongoDB 在新增資料的時候,會預設加上一個 _id
的欄位。
這個 _id
的欄位在 TypeScript 的中的 Type 是 mongoose.ObjectId
。
要驗證某個字串或數值是不是 MongoDB 的 ObjectId 可以使用:
import mongoose from 'mongoose'
const id = 'xxx'
const isValid = mongoose.Types.ObjectId.isValid(id) // return boolean
好處是,如果要用 id 進行資料庫的查詢,如果 id 的格式不對,那就可以直接進行例外處理,不用進行資料庫的操作。
在 MongoDB 中會自動加上一個 __v
的欄位,來記錄資料的變更。
可以定義 toJSON
這個 hook,加上 { versionKey: false }
,來讓 __v
消失。
MongoDB 預設會使用 _id
來當作主鍵。
但我們在定義 domain 或口語上可能還是會希望叫做 id
,而不是 _id
。
這時候定義 toJSON
這個 hook 的 virtuals
選項就很好用:
catSchema.set('toJSON', {
virtuals: true,
versionKey: false
})
可以觀察到 __v
的欄位不見了,
並且多了 id
這個欄位,其內容跟 _id
相同。
mongoose 不是幫你在資料庫新增欄位,只是在把資料轉成 JSON 的時候多一個虛擬的 id
,意思是,在程式中也可以透過 .id
來進行操作,非常便利。