在 MongoDB 裡每一筆資料我們稱為 Document,Schema 就是在 替 Document 規劃有哪些欄位以及這些欄位的規則 ,實作的方式很簡單,我們以會員為測試目標,先新增一個名為 models
的資料夾,用來存放所有資料模型,並在該資料夾下新增一個 user
資料夾,用來存放會員的 Schema,所以現在的目錄結構如下:
├── src
| ├── index.ts
| ├── app
| | └── app.routing.ts
| ├── models // 新增的目錄在這裡
| | └── user
| | └── user.schema.ts
| ├── database
| | ├── database.ts
| | └── index.ts
| └── environments
| ├── development.env
| └── production.env
├── package.json
└── tsconfig.json
先簡單定義一下會員的欄位有哪些,然後加到 user.schema.ts
中:
import mongoose from 'mongoose';
import { EmailValidator } from '../../validators';
export const UserSchema = new mongoose.Schema(
{
username: {
type: String,
required: true,
minlength: 6,
maxlength: 16
},
email: {
type: String,
required: true,
validate: {
validator: EmailValidator
}
}
}
);
從上方程式碼可以看出 Schema 的設置是透過 Object 設定並帶入 mongoose.Schema
產生實例,該 Object 名為 definition,如果這邊用 JavaScript 寫的話會沒辦法看到有哪些屬性可以填,還好我們用 TypeScript
definition 有許多屬性可以配置,我最常使用的大致上是這些:
type
:定義欄位的型別required
:是否為必填欄位min
:設置最小值max
:設置最大值minLength
:設置最小長度maxLength
:設置最大長度validate
:自定義的驗證器enum
:設置列舉值default
:設置預設值上方的程式碼有 EmailValidator
這個 Function,這是自訂的,我將它放在 src
下新增的 validators
資料夾中,檔案名稱為 email.validator.ts
:
export const EmailValidator = (email: string) => /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi.test(email);
並透過 index.ts
集中管理:
export * from './email.validator';
目前的資料夾結構如下:
├── src
| ├── index.ts
| ├── app
| | └── app.routing.ts
| ├── models
| | └── user
| | └── user.schema.ts
| ├── database
| | ├── database.ts
| | └── index.ts
| ├── environments
| | ├── development.env
| | └── production.env
| └── validators // 新增的目錄在這裡
| ├── email.validator.ts
| └── index.ts
├── package.json
└── tsconfig.json
由於 Schema 只是定義出 Document 的結構與限制條件,所以在完成 Schema 之後是無法直接使用的,可以想像 Schema 是一個人體但沒有靈魂,無法與他人溝通,那要怎麼賦予靈魂讓它能夠溝通呢?答案就是用 Model。
Model 將 Schema 轉換成可以操作 MongoDB 的樣子,方便我們進行 CRUD。而 Model 產生實例後就是 Document,這邊幫大家整理一張圖以了解它們之間的關係:
現在用 UserSchema
來建立 Model 吧!在 models
裡的 user
資料夾中新增 user.model.js
,並輸入下方程式碼:
import mongoose from 'mongoose';
import { UserSchema } from './user.schema';
export const UserModel = mongoose.model('User', UserSchema);
mongoose.model
第一個參數為 Collection 的名稱,第二個為 Schema,設置好就進行匯出。什麼?就這樣?是的就只有這樣!
然後在 user
資料夾下新增 index.ts
來集中管理:
export * from './user.model';
現在我們的資料夾結構如下:
├── src
| ├── index.ts
| ├── app
| | └── app.routing.ts
| ├── models
| | └── user
| | ├── user.schema.ts
| | ├── user.model.ts
| | └── index.ts
| ├── database
| | ├── database.ts
| | └── index.ts
| ├── environments
| | ├── development.env
| | └── production.env
| └── validators
| ├── email.validator.ts
| └── index.ts
├── package.json
└── tsconfig.json
Model 其實有提供靜態方法與方法的擴充,但我覺得不好維護,所以這邊就不特別寫用法了,如果有興趣的話可以點這裡看相關資訊。
廢話不多說,直接進入 業配 主題!
在 app.routing.ts
中,新增一個 POST 方法且路徑為 /users
的路由,並在這裡將 Model 實例化,再透過 Document 提供的 save()
方法來儲存:
router.post('/user', express.json(), errorHandler(async (req, res, next) => {
const { username, email } = req.body;
const user = new UserModel({ username, email });
const data = await user.save();
res.send(data);
}));
透過 Postman 進行測試:
可以看到出現了錯誤訊息,因為我們前面設定了 username
最少要 6 個字,修正一下就會通過了:
在 app.routing.ts
中,新增一個 GET 方法且路徑為 /users
的路由,透過 Model 提供的 find
方法來取得指定條件的 Documents:
router.get('/users', errorHandler(async (req, res, next) => {
const documents = await UserModel.find({ username: req.query.username });
res.send(documents);
}));
若
find()
不帶條件的話,會取該 Collection 下的所有 Document
結果如下,會返回一個陣列,裡面就是符合條件的 Documents:
上方的範例可以看出一個問題,明明是要透過 username
找特定使用者,為什麼要從所有的 Document 中找出所有符合該條件的資料呢?殺雞焉用牛刀!所以我們可以用 findOne
這個方法來 找出匹配條件的第一個結果,減少資料庫負擔:
router.get('/users', errorHandler(async (req, res, next) => {
const documents = await UserModel.findOne({ username: req.query.username });
res.send(documents);
}));
這樣就只會回傳該結果,而非陣列:
在 app.routing.ts
中,新增一個 PATCH 方法且路徑為 /users/:id
的路由,透過 Model 提供的 updateOne
方法來更改指定條件的 Document,這邊要特別注意的是,update 系列的方法 預設是不啟用驗證器的 ,若要啟用須設置 runValidators
為 true
:
router.patch('/users/:id', express.json(), errorHandler(async (req, res, next) => {
await UserModel.updateOne({ _id: req.params.id }, { username: req.body.username }, { runValidators: true });
res.send('成功');
}));
這邊為什麼沒有去接 updateOne
的回傳值來拿到更新後的值呢?因為 updateOne
只提供更新的功能,並 不會去取得更新後的值 ,若要取得更新後的值應該使用 findOneAndUpdate
,並設置參數 new
為 true
:
router.patch('/users/:id', express.json(), errorHandler(async (req, res, next) => {
const options: QueryFindOneAndUpdateOptions = {
new: true,
runValidators: true
};
const document = await UserModel.findByIdAndUpdate(req.params.id, { username: req.body.username }, options);
res.send(document);
}));
結果如下,有成功回傳更新後的資料:
在 app.routing.ts
中,新增一個 DELETE 方法且路徑為 /users/:id
的路由,透過 Model 提供的 findByIdAndRemove
方法來刪除指定條件的 Document:
router.delete('/users/:id', errorHandler(async (req, res, next) => {
await UserModel.findByIdAndRemove(req.params.id);
res.send('刪除成功');
}));
結果如下:
mongoose 的 CRUD 其實有非常多的內容可以學,但為了不離題,這邊主要是告訴大家 mongoose 的使用方法 ,以方便我們後面開發,所以詳細的操作方式可以去看官方文件,這部分還請多包涵。
下一篇將會進入到本系列的重點:用 MVC 架構來撰寫 Express 的專案,並盡可能技巧性地減少重複的程式碼,如果忘記 MVC 架構的人可以複習一下,那就期待下篇囉!