Mongoose是什麼?快速開發MongoDB與Express後端教學

Mongoose是什麼?快速開發MongoDB與Express後端教學

隨著 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(前置/後置鉤子)在資料儲存前後執行一些輔助邏輯,避免在程式碼各處重複檢查。

初始環境與安裝

在正式開始之前,以下列出本教學需要的基礎環境:

  1. Node.js(建議 14+ 以上版本)
  2. MongoDB(可在本地端安裝或使用雲端服務 MongoDB Atlas)
  3. npmYarn(本篇以 npm 範例為主)

專案初始化

  1. 建立一個資料夾:
    mkdir example-mongoose
    cd example-mongoose
    
  2. 初始化專案:
    npm init -y
    
  3. 安裝相關套件(npm install mongoose):
    npm install express mongoose
    
  4. 如果需要在開發階段自動重啟伺服器,可以安裝 nodemon:
    npm install --save-dev nodemon
    
  5. 修改 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。完成後,你可以在 ConnectDrivers 區塊中複製連線字串,例如:

mongodb+srv://<username>:<password>@cluster0.zyfzacs.mongodb.net/?retryWrites=true&w=majority

替換成你的帳號與密碼,在程式中寫法如下:

const mongoose = require('mongoose');

mongoose.connect('mongodb+srv://YOUR_USER:YOUR_PASSWORD@cluster0.zyfzacs.mongodb.net/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 最常用兩種方式來處理關聯:

  1. Reference(使用 ObjectId ),可搭配 populate() 進行關聯文件的自動填充。
  2. 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

資料來源

返回頂端