iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

現在就學Node.js系列 第 20

Mongoose 驗證、Hooks、關聯 (Population) -Day 20

  • 分享至 

  • xImage
  •  

昨天我們學會了 Mongoose 的 基本用法:Schema、Model 以及 CRUD 操作。

今天要進一步探討三個實務開發中非常重要的功能:

  1. 驗證與錯誤處理
  2. Hooks(中介函式)
  3. 關聯 (Population)

這些功能能讓我們的專案更有結構、更安全,也更貼近真實應用。

1. 驗證與錯誤處理

在真實專案中,不能讓使用者隨便丟資料進來。例如:

{ "name": 123, "email": "not-an-email" }

如果沒有驗證,這些髒資料會讓資料庫變得混亂。

Mongoose 提供了強大的 Schema 驗證,可以在寫入前就檢查。

常見驗證規則

const userSchema = new mongoose.Schema({
  name: { type: String, required: true, minlength: 2 },
  email: { type: String, required: true, unique: true, match: /.+\@.+\..+/ },
  age: { type: Number, min: 0, max: 120 }
});

  • required: true → 必填欄位
  • minlength / maxlength → 字串長度限制
  • match → 用正則表達式檢查格式(例如 email)
  • min / max → 數值範圍

錯誤處理

Mongoose 驗證失敗會丟出 ValidationError,可以用 try/catch 捕捉:

try {
  await User.create({ name: "A", email: "invalid" });
} catch (err) {
  console.error("❌ 驗證錯誤:", err.message);
}

Express API 裡,可以統一處理錯誤回傳:

app.use((err, req, res, next) => {
  if (err.name === "ValidationError") {
    return res.status(400).json({ error: err.message });
  }
  res.status(500).json({ error: "伺服器錯誤" });
});

2. Hooks(中介函式 / Middleware)

有時候我們希望在 資料存入前或存入後,自動做一些處理。

例如:在儲存使用者時,自動把密碼加密

pre 與 post

userSchema.pre("save", function (next) {
  console.log("🔒 即將儲存使用者:", this.name);
  next();
});

userSchema.post("save", function (doc, next) {
  console.log("✅ 已成功儲存:", doc._id);
  next();
});

實務案例:密碼加密

import bcrypt from "bcrypt";

userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

好處是 不用每次寫 API 時都重複寫加密程式碼,商業邏輯被集中管理。

3. 關聯 (Population)

雖然 MongoDB 是 NoSQL,但 Mongoose 提供了 關聯 (populate) 功能,

讓我們能像 SQL JOIN 一樣,把資料連結起來。

一對多:User 與 Post

const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: { type: mongoose.Schema.Types.ObjectId, ref: "User" }
});

const Post = mongoose.model("Post", postSchema);
  • ref: "User" → 表示這個欄位參考的是 User Model
  • ObjectId → MongoDB 內建的唯一 ID

建立使用者與文章

const newUser = await User.create({
  name: "Alice",
  email: "alice@example.com",
  password: "123456"
});

await Post.create({
  title: "我的第一篇文章",
  content: "這是內容",
  author: newUser._id
});

查詢文章並帶出作者資訊

const posts = await Post.find().populate("author", "name email");
console.log(posts);

透過 populateauthor 欄位做關聯,能夠自動帶出該使用者的 nameemail的資料。

整合Express API 範例

介紹完三個功能之後,用個小範例來整合這三個功能

import express from 'express'
import mongoose from 'mongoose'
import bcrypt from 'bcrypt'

const app = express()
app.use(express.json())

// 連線 MongoDB
await mongoose.connect('mongodb://localhost:27017/testdb')

// User Schema
const userSchema = new mongoose.Schema({
  name: {type: String, required: true, minlength: 2},
  email: {type: String, required: true, unique: true, match: /.+\@.+\..+/},
  password: {type: String, required: true, minlength: 6},
})

// Hooks:儲存前加密密碼
userSchema.pre('save', async function (next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10)
  }
  next()
})

const User = mongoose.model('User', userSchema)

// Post Schema (關聯 User)
const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
})

const Post = mongoose.model('Post', postSchema)

// API:建立使用者
app.post('/users', async (req, res, next) => {
  try {
    const user = await User.create(req.body)
    res.status(201).json(user)
  } catch (err) {
    next(err)
  }
})

// API:建立文章
app.post('/posts', async (req, res, next) => {
  try {
    const post = await Post.create(req.body)
    res.status(201).json(post)
  } catch (err) {
    next(err)
  }
})

// API:查詢文章並帶出作者資訊
app.get('/posts', async (req, res) => {
  const posts = await Post.find().populate('author', 'name email')
  res.json(posts)
})

// 統一錯誤處理
app.use((err, req, res, next) => {
  if (err.name === 'ValidationError') {
    return res.status(400).json({error: err.message})
  }
  res.status(500).json({error: '伺服器錯誤'})
})

// ----------------------
//  測試用假資料 Seeder
// ----------------------
async function seedFakeData() {
  const userCount = await User.countDocuments()
  if (userCount > 0) {
    console.log('📌 假資料已存在,跳過 Seeder')
    return
  }

  const fakeUsers = [
    {name: 'Alice', email: 'alice@example.com', password: 'password123'},
    {name: 'Bob', email: 'bob@example.com', password: 'password123'},
    {name: 'Charlie', email: 'charlie@example.com', password: 'password123'},
  ]

  for (const u of fakeUsers) {
    const user = new User(u)
    await user.save() // 觸發 pre("save") → 自動加密
  }

  console.log('✅ 假資料建立完成')
}
seedFakeData()

app.listen(3000, () => console.log('🚀 伺服器運行中:http://localhost:3000'))

小結

今天我們進一步認識了 Mongoose 的功能:

  1. 驗證與錯誤處理 → 確保資料格式正確,避免髒資料。
  2. Hooks → 將商業邏輯集中化,例如 密碼加密。
  3. 關聯 (populate) → 讓不同集合之間能夠「串起來」。

透過這些功能,我們能打造出更穩健、更安全、也更好維護的應用程式。


上一篇
Mongoose 入門 — 更高效的 MongoDB 操作工具 - Day 19
下一篇
使用者密碼安全 — bcrypt 與登入驗證 - Day21
系列文
現在就學Node.js24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言