本篇要解決的問題
前二篇我們是用 MongoDB Shell,用命令的方式來對資料庫進行 CRUD(增刪查改),這篇是用 Node.js 的方式,寫程式碼來進行 CRUD。
寫這篇筆記文時,發現 MongoDB Shell 如果很熟,在 Node.js 上做 CRUD 是很容易上手的,因為方式上很多相同的地方,建議可以先看上二篇的筆記文後,再來看這篇:Read、Delete、Update。
連結資料庫
在執行增刪查改資料庫之前,我們要先用 Node.js 來連結 MongoDB。
假設我們的資料庫是「bookstore」,bookstore 下有一個 collection 叫「books」。
我們寫的 Node.js,使用 express 來架 server,所以在開始前,會先執行以下命令安裝需要的 package:
$ npm init -y $ npm install express $ npm install mongodb
我們把連結資料庫的部份,另存一個 db.js,方便管理。
db.js:
const { MongoClient } = require('mongodb');
let dbConnection;
const connectToDb = async () => {
try {
const dbUri = 'mongodb://localhost:27017/';
const collectionName = 'bookstore';
const client = await MongoClient.connect(dbUri);
dbConnection = client.db(collectionName);
console.log('Successfully connected to MongoDB.');
} catch (err) {
console.error('Failed to connect to MongoDB', err);
process.exit(1);
}
};
const getDb = () => {
if (!dbConnection) {
throw new Error('No database connection.');
}
return dbConnection;
};
module.exports = { connectToDb, getDb };比較安全的作法,是將 MongoDB 的連接字符串存在 .env,這邊為了示範程式碼,就直接寫出來。
接著我們主要的 app.js,就可以引用 express、db.js,來連結資料庫:
app.js:
const express = require('express');
const { ObjectId } = require('mongodb');
const { connectToDb, getDb } = require('./db');
const app = express();
app.use(express.json());
// connect to the database
let db;
(async () => {
try {
await connectToDb();
app.listen(3000, () => {
console.log('Server is running on port 3000');
db = getDb();
});
} catch (err) {
console.error('Unable to start the server', err);
process.exit(1);
}
})();上面這段執行後,後續就可以用 db.collection() 來對資料庫做增刪查改。
而以下二行,是後面會用到的,先 require 進來:
const { ObjectId } = require('mongodb');
app.use(express.json());Read 查詢資料庫
使用 find + limit + toArray
就跟 MongoDB Shell 一樣,用 find() 查資料。
toArray() 會將所有資料轉成陣列。
要注意的是,MongoDB Shell 預設只會給前 20 筆資料,因此即使資料量有幾千萬筆,也不怕一個命令下去,記憶體就崩了。
但 Node.js 執行 find() 時,是沒有這個預設的,所以記得要用 limit() 限制資料量。
app.get('/findLimit', (req, res) => {
db.collection('books')
.find()
.limit(5)
.toArray()
.then(books => res.status(200).json(books))
.catch(err => {
res.status(500).send('Error occurred');
});
});使用 find + forEach
forEach() 一次處理一筆資料,代表只有當前被處理的文檔佔用記憶體,從而避免了將大量數據一次性加載到記憶體中可能導致的壓力。
跟 toArray() 不同的地方就在於,forEach() 不會將游標(Cursor)中的所有文檔轉換成一個陣列。
如果資料量很大,建議使用 forEach,可以減少應用程序的記憶體使用量。
app.get('/forEach', (req, res) => {
const books = [];
db.collection('books')
.find()
.sort({ author: 1 })
.forEach(book => books.push(book))
.then(() => res.status(200).json(books))
.catch(err => {
res.status(500).send('Error occurred');
});
});find + project,取出的資料,只回傳特定字段
如果我們的 document 字段很多,而我們只需要其中的幾個字段,比方一本書的資料,字段可能有書名、出版社、出版日期、作者、譯者、頁數、分類……,我們在清單頁上不會全部顯示,可能只需要顯示書名、作者這二個字段的話,就用 project()。
app.get('/projection', (req, res) => {
db.collection('books')
.find()
.limit(3)
.project({ _id: 0, title: 1, author: 1 })
.toArray()
.then(books => res.status(200).json(books))
.catch(err => {
res.status(500).send('Error occurred');
});
});上面程式碼中的 projec(),可以改寫成:
app.get('/projection2', (req, res) => {
db.collection('books')
.find({}, { projection: { _id: 0, title: 1, author: 1 } })
.limit(3)
.toArray()
.then(books => res.status(200).json(books))
.catch(err => {
res.status(500).send('Error occurred');
});
});第一種方式比較靈活,因為後面還可以再加上其它運算子,實際使用就看個人習慣。
skip 分頁功能
我們實際維運一個站時,比方網路書店,資料會有幾千幾萬筆,不可能一個頁面就全部顯示出來,後端也不會做一次取全部 collection 內的 documents 這種事。
因此,分頁功能是很重要的,在 MonogoDB 中主要使用 skip() 來執行分頁。
app.get('/pagination', (req, res) => {
const page = parseInt(req.query.page) || 1;
const size = parseInt(req.query.size) || 10;
const books = [];
db.collection('books')
.find()
.skip((page - 1) * size)
.limit(size)
.forEach(book => books.push(book))
.then(() => res.status(200).json(books))
.catch(err => {
res.status(500).send('Error occurred');
});
});上面的程式碼中,我們主要讀取網址上的 page、size 這二個參數,page 是第幾頁,size 是指一頁有幾筆。
例如,網址為 /books?page=2&size=5,代表第 2 頁,一頁顯示 5 筆資料。
findOne,查詢單筆資料
清單頁上用 find(),因為一頁顯示需要多筆資料,而進到單頁後,只需要取一筆詳細資料就行。
只查詢單筆資料用 findOne():
app.get('/book/:id', (req, res) => {
const id = req.params.id;
// 檢查 id 是否為有效的 ObjectId
if (ObjectId.isValid(id)) {
db.collection('books')
.findOne({ _id: new ObjectId(id) })
.then(book => res.status(200).json(book))
.catch(err => {
res.status(500).send('Error occurred');
});
} else {
res.status(400).send('Invalid ID');
}
});我們使用網址上的第二段來當作查詢的 ID。
「檢查 id 是否為有效的 ObjectId」這個判斷式很重要,可以避免使用者亂輸入網址,或是網址已不存在時,會看見頁面錯誤。加上了判斷,就可以在遇到不存在的 ID 時,執行我們的設計,比方直接轉去 404 頁。
insert 新增資料
如果想新增單筆資料,用 insertOne():
app.post('/createBook', (req, res) => {
const book = req.body;
db.collection('books')
.insertOne(book)
.then(result => res.status(201).json(result))
.catch(err => {
res.status(500).send('Error occurred');
});
});如果想一次新增多筆資料,用 insertMany():
app.post('/createBooks', (req, res) => {
const books = req.body;
db.collection('books')
.insertMany(books)
.then(result => res.status(201).json(result))
.catch(err => {
res.status(500).send('Error occurred');
});
});updateOne 更新資料
app.patch('/updateBook/:id', (req, res) => {
const id = req.params.id;
const book = req.body;
if (ObjectId.isValid(id)) {
db.collection('books')
.updateOne({ _id: new ObjectId(id) }, { $set: book })
.then(result => res.status(200).json(result))
.catch(err => {
res.status(500).send('Error occurred');
});
} else {
res.status(400).send('Invalid ID');
}
});deleteOne 刪除資料
app.delete('/deleteBook/:id', (req, res) => {
const id = req.params.id;
if (ObjectId.isValid(id)) {
db.collection('books')
.deleteOne({ _id: new ObjectId(id) })
.then(result => res.status(200).json(result))
.catch(err => {
res.status(500).send('Error occurred');
});
} else {
res.status(400).send('Invalid ID');
}
});

