iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 12
1
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 12

[今晚我想來點 Express 佐 MVC 分層架構] DAY 12 - mongoose 之 CRUD

  • 分享至 

  • xImage
  •  

Schema

在 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/images/emoticon/emoticon07.gif

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

Model 將 Schema 轉換成可以操作 MongoDB 的樣子,方便我們進行 CRUD。而 Model 產生實例後就是 Document,這邊幫大家整理一張圖以了解它們之間的關係:
https://ithelp.ithome.com.tw/upload/images/20200822/201193384OcTcWexjq.png

現在用 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 其實有提供靜態方法與方法的擴充,但我覺得不好維護,所以這邊就不特別寫用法了,如果有興趣的話可以點這裡看相關資訊。

實作 CRUD

廢話不多說,直接進入 業配 主題!

新增 (Create)

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 進行測試:
https://ithelp.ithome.com.tw/upload/images/20200822/20119338y83Ln2OFHE.png

可以看到出現了錯誤訊息,因為我們前面設定了 username 最少要 6 個字,修正一下就會通過了:
https://ithelp.ithome.com.tw/upload/images/20200822/20119338waTyBTTggL.png

讀取 (Read)

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:
https://ithelp.ithome.com.tw/upload/images/20200822/20119338BMVg5Yhmeq.png

上方的範例可以看出一個問題,明明是要透過 username 找特定使用者,為什麼要從所有的 Document 中找出所有符合該條件的資料呢?殺雞焉用牛刀!所以我們可以用 findOne 這個方法來 找出匹配條件的第一個結果,減少資料庫負擔:

router.get('/users', errorHandler(async (req, res, next) => {
  const documents = await UserModel.findOne({ username: req.query.username });
  res.send(documents);
}));

這樣就只會回傳該結果,而非陣列:
https://ithelp.ithome.com.tw/upload/images/20200822/20119338TMdxlZAKXS.png

更新 (Update)

app.routing.ts 中,新增一個 PATCH 方法且路徑為 /users/:id 的路由,透過 Model 提供的 updateOne 方法來更改指定條件的 Document,這邊要特別注意的是,update 系列的方法 預設是不啟用驗證器的 ,若要啟用須設置 runValidatorstrue

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,並設置參數 newtrue

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);
}));

結果如下,有成功回傳更新後的資料:
https://ithelp.ithome.com.tw/upload/images/20200822/20119338ZSoiSlf3lP.png

刪除 (Delete)

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('刪除成功');
}));

結果如下:
https://ithelp.ithome.com.tw/upload/images/20200822/201193382gNPsesBJw.png

小結

mongoose 的 CRUD 其實有非常多的內容可以學,但為了不離題,這邊主要是告訴大家 mongoose 的使用方法 ,以方便我們後面開發,所以詳細的操作方式可以去看官方文件,這部分還請多包涵。
下一篇將會進入到本系列的重點:用 MVC 架構來撰寫 Express 的專案,並盡可能技巧性地減少重複的程式碼,如果忘記 MVC 架構的人可以複習一下,那就期待下篇囉!


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 11 - 用 mongoose 連線 MongoDB
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 13 - 規劃 Express 專案
系列文
今晚我想來點 Express 佐 MVC 分層架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言