iT邦幫忙

2022 iThome 鐵人賽

DAY 20
0
Modern Web

Fastify 101系列 第 20

[Fastify] Day20 - Mongoose

  • 分享至 

  • xImage
  •  

大家好,我是 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

我們可以透過 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 Validation

我們來實際嘗試一下,如果沒有符合 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 搭配

我們知道 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。

https://ithelp.ithome.com.tw/upload/images/20221005/20151148WXSZswivB6.jpg

透過 route 的 Validation,可以確保進來的格式正確。
透過 mongoose model 的 Schema Validation,可以確保進去 database 的格式正確,不用擔心在處理資料的過程把資料弄亂。

如此一來可以更加提升程式品質與工作體驗。


Others

mongoose.connect()

  • mongoose.connect() 連到某個 MongoDB Server
import 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)
    }
})

Type.ObjectId

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 的格式不對,那就可以直接進行例外處理,不用進行資料庫的操作。

versionKey

在 MongoDB 中會自動加上一個 __v 的欄位,來記錄資料的變更。

可以定義 toJSON 這個 hook,加上 { versionKey: false },來讓 __v 消失。

virtuals

MongoDB 預設會使用 _id 來當作主鍵。

但我們在定義 domain 或口語上可能還是會希望叫做 id,而不是 _id

這時候定義 toJSON 這個 hook 的 virtuals 選項就很好用:

catSchema.set('toJSON', {
    virtuals: true,
    versionKey: false
})

https://ithelp.ithome.com.tw/upload/images/20221005/2015114810Ov8ugjgC.jpg

可以觀察到 __v 的欄位不見了,
並且多了 id 這個欄位,其內容跟 _id 相同。

mongoose 不是幫你在資料庫新增欄位,只是在把資料轉成 JSON 的時候多一個虛擬的 id,意思是,在程式中也可以透過 .id 來進行操作,非常便利。


上一篇
[Fastify] Day19 - Database
下一篇
[Fastify] Day21 - Cache with Redis
系列文
Fastify 10130
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言