隨著 Node.js 與 JavaScript 在後端領域的盛行,使用 JavaScript 搭配 NoSQL 數據庫已成為後端開發的主流選擇之一。MongoDB 以文件型資料庫(Document-based database)著稱,資料結構與 JSON 相似;再加上 Mongoose 是一個 ODM(Object Data Modeling,也就是 Object Document Mapping),不僅能讓我們在程式中用物件導向的思維管理資料,同時還是能節省開發與維護時間的解決方案。
本篇文章將詳細介紹如何使用 Mongoose 連接與操作 MongoDB,並說明在 Express 專案中如何快速整合,包括最常用的 CRUD、Schema、Validation 等進階功能。文中也會示範一個完整的 ToDo List 後端範例,幫助讀者從零開始輕鬆掌握開發流程。除了文章內容,你也可以前往官網、翻看其他人學習筆記或參加線上課程來了解更多資訊。
什麼是 Mongoose 與 MongoDB?
1. MongoDB 簡述
- 文件型 NoSQL 資料庫:MongoDB 儲存結構類似 JSON 的 BSON(二進位 JSON),非常易於閱讀與維護。
- 彈性與擴充性:不需要像傳統 SQL Database 一樣預先定義固定的 Schema,資料欄位可以更動態,也更適合分散式架構下的橫向擴充。
- 易學易用:MongoDB 的查詢語法直覺且文檔完善。
2. Mongoose 簡述
- ODM(Object Data Modeling):Mongoose 將 MongoDB 的文件模型化,能將傳入或取出的文件映射成 JavaScript 物件,進一步提升程式可讀性與維護性,可以當作是個中介軟體。
- Schema 驅動:雖然 MongoDB 不強制每筆文件有一致的欄位,但若透過 Mongoose,可以在程式層就定義好「骨架」(Schema),包括定義類型、欄位型態、驗證規則等。
- 封裝增刪改查:Mongoose 提供了許多 CRUD 方法與進階功能,如 Model.find(), Model.updateOne(), Model.deleteMany(), populate() 等,開發者可快速上手。
- Validation 與 Middleware:可直接在 Schema 設定驗證規則(例如最小值、最大值、自訂錯誤訊息),也能撰寫 Middleware(前置/後置鉤子)在資料儲存前後執行一些輔助邏輯,避免在程式碼各處重複檢查。
初始環境與安裝
在正式開始之前,以下列出本教學需要的基礎環境:
- Node.js(建議 14+ 以上版本)
- MongoDB(可在本地端安裝或使用雲端服務 MongoDB Atlas)
- npm 或 Yarn(本篇以 npm 範例為主)
專案初始化
- 建立一個資料夾:
mkdir example-mongoose cd example-mongoose
- 初始化專案:
npm init -y
- 安裝相關套件(npm install mongoose):
npm install express mongoose
- 如果需要在開發階段自動重啟伺服器,可以安裝 nodemon:
npm install --save-dev nodemon
- 修改 package.json 中的 scripts:
{ "scripts": { "start": "node app.js", "server": "nodemon app.js" } }
其中 app.js 為主要程式進入點。日後只要執行 npm run server 就能以 nodemon 啟動專案。
連線到 MongoDB(包含本地端與雲端 Atlas)
本地端 MongoDB
若你已安裝過本地端 MongoDB,並開啟其預設埠(27017),在 app.js 或其他主程式裡面可以直接使用:
const mongoose = require('mongoose');
// 連線到本地端 MongoDB(mongoose connect),db 名稱為 exampleDB
mongoose.connect('mongodb://127.0.0.1:27017/exampleDB', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('Connected to local MongoDB'))
.catch((err) => console.log(err));
雲端 MongoDB Atlas
若想直接使用官方雲端服務,可以至 MongoDB Atlas 註冊免費帳號,並建立一個專屬的 Cluster。完成後,你可以在 Connect → Drivers 區塊中複製連線字串,例如:
mongodb+srv://<username>:<password>@cluster0.zyfzacs.mongodb.net/?retryWrites=true&w=majority
替換成你的帳號與密碼,在程式中寫法如下:
const mongoose = require('mongoose');
mongoose.connect('mongodb+srv://YOUR_USER:[email protected]/exampleDB?retryWrites=true&w=majority')
.then(() => console.log('Connected to MongoDB Atlas'))
.catch((err) => console.log(err));
註:實務中要注意IP 存取白名單與帳號密碼的安全性,否則可能讓所有人都能連到你的資料庫。
Schema 與 Model:定義資料結構
1. 建立 Schema
Schema 可以想像成定義此「文件集合」的結構與規則,包含欄位型態、預設值、驗證規則等。例如:
// models/student.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const studentSchema = new Schema({
name: {
type: String,
required: true // 必填
},
age: {
type: Number,
min: [0, 'age cannot be negative'] // 設定最小值
},
major: {
type: String,
default: 'Undecided'
},
scholarships: {
merit: Number,
other: Number
}
});
module.exports = mongoose.model('Student', studentSchema);
在這裡我們定義:
- name 欄位必填(required: true)。
- age 不能小於 0,若違反則顯示錯誤訊息 ‘age cannot be negative’。
- major 預設值 ‘Undecided’。
- scholarships 是個巢狀物件,有 merit 與 other 兩個子欄位。
2. 建立 Model
建立完 Schema 後,我們能透過 mongoose.model(‘集合名稱’, schema物件) 來產生可操作的 Model。如上例已經匯出 Model:
module.exports = mongoose.model('Student', studentSchema);
- 第一個參數 Student:是此模型的「單數名稱」,Mongoose 實際會轉為 students 儲存在資料庫中。
- 第二個參數就是我們定義的 studentSchema。
注意:如果你未指定 collection 名,Mongoose 會自動將 ‘Student’ 轉小寫+加 s -> ‘students’ 作為實際的集合名稱。
CRUD 基本操作
CRUD 即 Create(建立)、Read(查詢)、Update(更新)、Delete(刪除)。以下皆使用我們的 Student 模型為例,展示最典型的 Mongoose 用法。
建立 Create
1. 建立單筆文件
const Student = require('./models/student');
// 先以 new 建立一個模型實例
const newStudent = new Student({
name: 'Alice',
age: 20,
major: 'Computer Science',
scholarships: {
merit: 3000,
other: 1000
}
});
// 使用 .save() 儲存到資料庫
newStudent.save()
.then(savedDoc => {
console.log('New student created:', savedDoc);
})
.catch(err => {
console.error(err);
});
console log
2. 建立多筆文件
Student.insertMany([
{ name: 'Bob', age: 25 },
{ name: 'Cherry', age: 18, major: 'Geography' }
])
.then(res => {
console.log('Multiple docs inserted:', res);
})
.catch(err => console.error(err));
查詢 Read
1. 查詢全部或根據條件篩選
// 找出全部學生
Student.find({})
.then(docs => console.log(docs))
.catch(err => console.error(err));
// 找出年齡大於等於 20 的學生
Student.find({ age: { $gte: 20 } })
.then(docs => console.log(docs))
.catch(err => console.error(err));
- Student.find() 回傳包含多筆結果的陣列,如果無符合條件,回傳空陣列。
2. 查詢單筆資料
// 找到第一筆符合條件的文件
Student.findOne({ name: 'Alice' })
.then(doc => console.log(doc))
.catch(err => console.error(err));
// 使用 _id 查詢單筆
Student.findById('64d8f24e1b2fcd02db833370')
.then(doc => {
if (!doc) {
console.log('No student found!');
} else {
console.log(doc);
}
})
.catch(err => console.error(err));
- Student.findOne() 只會回傳第一筆符合條件的文件。
- Student.findById() 快速以 _id 查詢 MongoDB 自動生成的唯一值。
3. 進階查詢與連鎖語法
Mongoose 提供 .where()、.sort()、.limit() 等多種操作:
Student.find()
.where('age').gte(18).lte(25) // 18 <= age <= 25
.limit(5)
.sort({ age: -1 }) // 依年齡大到小排序
.select('name age') // 只返回 name, age 欄位
.exec()
.then(docs => console.log(docs))
.catch(err => console.error(err));
- 可讀性高,方便鏈式呼叫。
更新 Update
1. updateOne() 與 updateMany()
// updateOne(篩選條件, 更新內容, [選項])
Student.updateOne({ name: 'Alice' }, { $set: { major: 'Math' } })
.then(msg => console.log(msg))
.catch(err => console.error(err));
// updateMany(篩選條件, 更新內容)
Student.updateMany({ age: { $lt: 18 } }, { $set: { age: 18 } })
.then(msg => console.log(msg))
.catch(err => console.error(err));
- updateOne() 只會更新找到的第一筆。
- updateMany() 則更新所有符合條件的筆數。
2. findOneAndUpdate()
如果想同時取回更新前或更新後的文件,可以使用 findOneAndUpdate() 或 findByIdAndUpdate():
Student.findOneAndUpdate(
{ name: 'Bob' },
{ age: 26 },
{ new: true, runValidators: true } // new: true 回傳更新後文件; runValidators: true 表示執行驗證
)
.then(updatedDoc => console.log('Updated doc:', updatedDoc))
.catch(err => console.error(err));
- 預設 new 為 false,即回傳更新前的文件。如果想在 .then() 取到更新後值,就需將 new 設為 true。
- runValidators: true 確保更新時也要跑 Schema 驗證規則。
刪除 Delete
1. deleteOne() 與 deleteMany()
// 刪除第一筆符合條件的文件
Student.deleteOne({ name: 'Alice' })
.then(result => console.log(result))
.catch(err => console.error(err));
// 刪除所有符合條件的文件
Student.deleteMany({ age: { $gte: 30 } })
.then(result => console.log(result))
.catch(err => console.error(err));
- deleteOne() 與 deleteMany() 在刪除成功後,會回傳一個包含 deletedCount 的物件。
2. findByIdAndRemove()
Student.findByIdAndRemove('64d8f24e1b2fcd02db833370')
.then(deletedDoc => {
if (!deletedDoc) {
console.log('No doc found to delete');
} else {
console.log('Deleted doc:', deletedDoc);
}
})
.catch(err => console.error(err));
- 使用 _id 刪除並回傳被刪除的文件(若找不到會回傳 null)。
其他常用功能與進階配置
Validation 驗證
Mongoose 提供各式預設與自訂驗證器:
- required:必填
- enum:只允許特定字串值
- min / max:針對數字
- match:針對字串匹配正規表達式
- 自訂 validator:可自定函式
例如:
const bookSchema = new Schema({
title: { type: String, required: [true, 'Title is required'] },
author: { type: String, required: true },
year: {
type: Number,
min: [0, 'Year cannot be negative'],
max: [2100, 'Year is too large']
},
category: {
type: String,
enum: ['Fiction', 'Non-fiction', 'Other']
}
});
驗證失敗時,.save() 或 .update() 就會拋出錯誤,可用 try…catch 捕捉並回傳適當的錯誤資訊給前端。
Options 設定與執行流程
在更新/查詢資料時,可以透過第三個參數指定 Options 物件,比如:
Student.findOneAndUpdate(
{ name: 'Alice' },
{ $inc: { age: 1 } },
{
new: true, // 回傳更新後的文件
runValidators: true, // 執行驗證器
upsert: false // 若找不到是否要新建
}
);
- upsert: true 有時用於「若無則新增」的場景。
Instance Methods 與 Static Methods
1. Instance Methods(實例方法)
例如我們想在每個 Student 文件中都提供一個方法 printTotalScholarships():
studentSchema.methods.printTotalScholarships = function() {
return (this.scholarships.merit || 0) + (this.scholarships.other || 0);
};
如此一來,就能在任何透過 Model 取得的 Student 實例上呼叫:
Student.findOne({ name: 'Alice' })
.then(stu => {
console.log(stu.printTotalScholarships());
});
2. Static Methods(靜態方法)
針對整個 Model,可能想要額外封裝一些查詢操作。例如:
studentSchema.statics.findAllMajorStudents = function(major) {
return this.find({ major });
};
使用方式:
Student.findAllMajorStudents('Math')
.then(docs => console.log(docs))
.catch(err => console.error(err));
以上能讓程式碼更具可讀性與維護性。
Middleware 中介層
Mongoose 的 Middleware(又稱 Hook,屬於中介軟體範疇)可在文件 save、remove、validate、updateOne 等操作前或後執行一些自動化流程。例如,我們想在每次新增 Student 之前都寫個紀錄到檔案:
const fs = require('fs');
studentSchema.pre('save', function(next) {
fs.writeFile('record.txt', 'A new student data will be saved.\n', (err) => {
if (err) throw err;
next(); // 必須呼叫 next() 讓流程繼續
});
});
還有 post(‘save’)、pre(‘remove’)、post(‘remove’) 等許多場景可用,用來進行日誌記錄、資料驗證、版本控管等。
範例:建立一個簡易的待辦清單(ToDo List)API
以下以 Express + Mongoose 示範最簡單的 RESTful API,包含「查詢全部」「查詢單筆」「新增」「更新」「刪除」。
1. 專案結構
example-mongoose
├── app.js
├── models
│ └── todo.js
└── routes
└── todo.js
- app.js:應用程序主程式,處理 Express 啟動、資料庫連線、路由載入。
- models/todo.js:Todo Schema 與 Model。
- routes/todo.js:各項 RESTful API 的路由實作。
2. 建立 models/todo.js
// models/todo.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const todoSchema = new Schema({
thing: {
type: String,
required: true
},
isDone: {
type: Boolean,
required: true,
default: false
},
createdDate: {
type: Date,
default: Date.now,
required: true
}
});
module.exports = mongoose.model('Todo', todoSchema);
3. 建立 routes/todo.js
// routes/todo.js
const express = require('express');
const router = express.Router();
const Todo = require('../models/todo');
// 1. 取得全部待辦
router.get('/', async (req, res) => {
try {
const todos = await Todo.find();
res.json(todos);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// 2. 取得單一待辦
router.get('/:id', getTodo, (req, res) => {
// 透過中介函式 getTodo 預先將 Todo 文件存入 res.todo
res.json(res.todo);
});
// 3. 新增待辦
router.post('/', async (req, res) => {
const todo = new Todo({
thing: req.body.thing,
isDone: req.body.isDone
});
try {
const newTodo = await todo.save();
res.status(201).json(newTodo);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// 4. 更新待辦
router.patch('/:id', getTodo, async (req, res) => {
if (req.body.thing != null) {
res.todo.thing = req.body.thing;
}
if (req.body.isDone != null) {
res.todo.isDone = req.body.isDone;
}
try {
const updatedTodo = await res.todo.save();
res.json(updatedTodo);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// 5. 刪除待辦
router.delete('/:id', getTodo, async (req, res) => {
try {
await res.todo.deleteOne();
res.json({ message: 'Deleted This Todo' });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// 中介函式:取得指定 id 的 Todo 物件
async function getTodo(req, res, next) {
let todo;
try {
todo = await Todo.findById(req.params.id);
if (todo == null) {
return res.status(404).json({ message: 'Cannot find todo' });
}
} catch (err) {
return res.status(500).json({ message: err.message });
}
res.todo = todo; // 存入 res 傳給後續路由使用
next();
}
module.exports = router;
4. 建立 app.js
// app.js
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// 連線資料庫:local 或 Atlas 均可
mongoose.connect('mongodb://127.0.0.1:27017/exampleDB')
.then(() => console.log('Connected to MongoDB'))
.catch((e) => console.log(e));
// 為了能解析 JSON Body
app.use(express.json());
// Router
const todoRouter = require('./routes/todo');
app.use('/todo', todoRouter);
// 啟動伺服器
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
5. 測試 API
- GET http://localhost:3000/todo:查所有代辦
- GET http://localhost:3000/todo/:id:查單筆
- POST http://localhost:3000/todo
Body(JSON):{ "thing": "學習 Mongoose", "isDone": false }
- PATCH http://localhost:3000/todo/:id
Body(JSON):{ "isDone": true }
- DELETE http://localhost:3000/todo/:id
若全部正確,便能透過此簡易 API 完成 CRUD 流程。透過這些操作,我們也能更直覺地掌握 MongoDB 與 Mongoose 的使用概念與實務應用。
常見問題與解答(FAQ)
Q1:Mongoose 與直接使用 MongoDB Driver(mongodb 套件)有何差異?
A:直接使用 MongoDB Driver,能獲得較底層的操控與潛在的效能優勢,但可能需要自己處理 Schema、驗證、錯誤管理等實務問題。Mongoose 則提供了一層高階抽象,使資料操作像使用物件導向,減少重複程式碼與維護成本。如果你想更自由地操作各種聚合管線或複雜指令,MongoDB Driver 可以更細節;若專案需要穩定結構與可維護性,或需要在應用層做模型驗證,Mongoose 會是好選擇。
Q2:若我的 Schema 與儲存的文件結構不一致會發生什麼事?
A:MongoDB 其實不會阻止你寫入其他欄位,但如果使用 Mongoose,超出 Schema 設計的欄位預設不會出現在實例物件裡。如果你設置 strict: true(Mongoose 預設),未在 Schema 中定義的欄位通常不會被保存。若你刻意想保留,就要在 Schema 選項加上 strict: false,或使用 Mixed 數據類型。
Q3:為什麼在 updateOne() 時無法觸發驗證?
A:Mongoose 預設在 updateOne()、findByIdAndUpdate() 等不會執行驗證。如果想要執行 Schema Validation,要在第三個參數 { runValidators: true };或者先把文件查出改值再 save()。
Q4:對於關聯關係較複雜的情況,如一對多、多對多,我是否能用 Mongoose?
A:可以!Mongoose 最常用兩種方式來處理關聯:
- Reference(使用 ObjectId ),可搭配 populate() 進行關聯文件的自動填充。
- Embed(將另一個文件嵌入欄位),在讀取大量巢狀結構時效率更高,但更新相對不如引用靈活。
請依專案需求選擇合適模式。
Q5:Mongoose 是否支援交易(Transaction)?
A:在 MongoDB v4.0+ 版本,針對副本集(Replica Set)或分片集群都可使用交易機制。Mongoose 也支援透過 session 來執行多步驟原子操作。例如:
const session = await mongoose.startSession();
session.startTransaction();
try {
// 多個 Model 操作
await A.create([{ ... }], { session });
await B.updateOne(..., { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
} finally {
session.endSession();
}
能確保多個資料操作要麼全部成功,要麼全部回滾,保持一致性。
總結
Mongoose 作為 Node.js 與 MongoDB 之間的 ODM,不僅提供了更直覺的物件導向操作模式,也整合了許多實務中必用的功能(例如 Schema、Validation、Middleware 及 Query Helpers 等),在中大型專案中特別能降低維護成本。
本篇教學從安裝到連線、再到定義 Schema 與執行 CRUD、進階操作、最後展示一個簡易的 ToDo List 範例,希望你能透過範例進一步了解 Mongoose 如何為 MongoDB 帶來更好的結構與可讀性。如果你對 Mongoose 的其它功能(例如聚合管線、Transaction、Static/Instance Method)有興趣,建議可以從官方文件深入了解並多加練習。祝各位開發順利!
Schema 常見欄位屬性對照
屬性 | 型態 / 用途 | 範例 |
---|---|---|
type | 欄位的資料型態 | type: String, type: Number, type: Boolean, type: Date |
required | 必填欄位 | required: true 或 required: [true, ‘錯誤訊息’] |
default | 欄位預設值 | default: Date.now 或 default: ‘someValue’ |
min / max | 數字最小值、最大值 | min: [0, ‘Value too small’], max: 100 |
enum | 只能是指定清單中的值(String) | enum: [‘Coffee’, ‘Tea’, ‘Water’] |
match | 字串需符合正規表示式 | match: /pattern/ 或 match: [/pattern/, ‘message’] |
validate | 客製化驗證 | validate: { validator(v){…}, message: ‘…’ } |
unique | 是否唯一索引(需搭配 DB 索引) | unique: true |
uppercase | 將字串自動轉大寫 | uppercase: true |
lowercase | 將字串自動轉小寫 | lowercase: true |
trim | 移除字串前後空白 | trim: true |
select | 查詢時預設是否回傳該欄位 | select: false |
資料來源
- Mongoose 入門(簡介、CRUD、實例&靜態方法、中介軟體) | by 拉爾夫的技術隨筆 | Medium
- Express 教學 3:使用資料庫(Mongoose) – 學習 Web 開發 | MDN
- Mongoose 是什麼: 資料操作三步驟從 MongoDB 到 API | 前端三分鐘 | 一起用三分鐘分享技術與知識