本篇要解決的問題
前二篇我們是用 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'); } });

