Jeff的隨手筆記

學習當一個前端工程師

0%

第一次接觸mongoose

消失了一個月,終於回來了~~雖然有點晚但祝大家新年快樂~~

整個12月懶惰病又發作了,天氣冷導致每天早上都爬不起來,表定22點下班都拖到20分後才讓我走(還沒有加班費…),導致回家完全都不想看書,但說實在這都是藉口啦XDD

這次是做了一個餐廳的簡單系統,裡面包含了點餐、員工打卡、菜單的CRUD、會員系統,原本計畫是要做一個客戶端跟商家端並且在12月底完成,結果我只完成商家端的而且還拖到1月初。

報告完近況來說一下這篇的主題,這次練習是用Next JS 來練習,最主要的原因就是它可以建構出API,畢竟我沒有學過後端。

這篇就是來複習在建構這次專案的API時,我所使用到的最基礎的語法。讓我們開始吧!

Mongoose 是什麼?

什麼是Mongoose?

根據維基百科的說明:Mongoose是一個JavaScript 物件導向的程式庫,它在MongoDB和Node.js JavaScript 執行階段環境之間建立連接。

因此我們可以透過使用Mongoose 來操作MongoDB ,讓我們透過一些它封裝好的方法來操作資料庫,這樣我們就不用自己寫原生的 MongoDB 指令。

那為什麼要用NoSQL 資料庫的MongoDB 呢?原因很簡單:它免費!

如何用 Mongoose來連接 MongoDB的資料庫?

要連接到資料庫非常簡單,我們只需要使用**mongoose.connect() 這個語法就可以了。

當程式執行到這一行的指令時,就會與資料庫連線。在這裡我們需要告知程式要去哪些尋找資料庫,因此需要傳入連線字串。

正常來說我們應該是會直接這樣使用:

1
mongoose.connect('mongodb://username:password@host:port/database?options...');

至於要怎麼拿到這一串,大家可以參考這篇文章

但連線字串裡其實包含了「帳號」與「密碼」這類的敏感資訊,通常我們不會想把這類型的資訊明明白白地寫在程式碼裡。

因此在官網上有教我們使用process.env 的方式來儲存這些資訊,這邊我就使用專案的部分內容來舉例:

首先我們先設定env:

在vs code的最上層建立個資料夾,名稱是 .env

圖片

裡面的內容放上我們要連接的資料庫:

1
MONGODB_URI='mongodb://username:password@host:port/database?options...'

然後當我們要使用資料庫的時候,就可以這樣使用:

1
const client = await MongoClient.connect(`${process.env.MONGODB_URI}`);

這樣我們就可以不透露「帳號」與「密碼」這類的敏感資訊。

程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// db.tsx
import { MongoClient } from "mongodb";

export async function connectToDatabase() {
const client = await MongoClient.connect(`${process.env.MONGODB_URI}`);
return client;
}

// API.tsx
import { connectToDatabase } from "@/services/db";
import type { NextApiRequest, NextApiResponse } from "next";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const client = await connectToDatabase();
const db = client.db();

client.close();
}

export default handler;

這個基本上就是我在這個專案所有API的起手式,把連接資料庫的方法製作成**db.tsx** 一個文件,讓要連接資料庫時只需要導入這個文件即可。

p.s.我的疑惑:MongoClient.connect() 跟直接用mongoose.connect()的差別在哪裡

在網路上查了很久但還是沒有找到

MongoDB做 CRUD

當連線到資料庫後,接下來就是要對資料庫做CRUD了。

簡單解釋一下CRUD:

  • C = Create 新增、建立或是創建
  • R = Read 讀取、查詢
  • U = Update 更新
  • D = Delete 刪除

那接下來讓我們來看看,在mongoose 要怎麼操作:

MongoDB做 CRUD 之 Create

雖然 Create 代表著新增、建立或是創建的意思,但我們實際上使用的語法則是 insertion。

db.collection.insertOne()db.collection.insertMany() 都是 MongoDB 中用於插入文檔(documents)到集合(collection)中的方法,但它們之間有一些主要的區別,接下來讓我們認識一下這兩個語法:

db.collection.insertOne(document, options)

  • 單個文檔insertOne 用於插入單個文檔。

  • 參數:接受兩個參數,第一個是要插入的文檔(document),第二個是一個選項對象(options),用於指定插入的一些選項。

  • 返回值:返回一個 InsertOneResult 對象,其中包含插入操作的結果信息,例如插入的文檔的 _id

  • 範例

    1
    db.collection.insertOne({ name: 'John', age: 30 });

db.collection.insertMany(documents, options)

  • 多個文檔insertMany 用於插入多個文檔,接受一個文檔數組作為參數。

  • 參數:同樣接受兩個參數,第一個是一個文檔數組,第二個是一個選項對象,用於指定插入的一些選項。

  • 返回值:返回一個 InsertManyResult 對象,其中包含插入操作的結果信息,例如插入的文檔的 _id

  • 範例

    1
    2
    3
    4
    5
    db.collection.insertMany([
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 35 },
    { name: 'Charlie', age: 40 }
    ]);

區別和適用場景:

  • 性能insertOne 通常比 insertMany 更快,因為它處理的文檔數量較少。
  • 使用場景
    • 使用 insertOne,當你只想插入單個文檔時。
    • 使用 insertMany,當你需要一次性插入多個文檔時,這樣可以減少與數據庫的通信次數,提高效率。

案例:

我們用這次專案的建立使用者來做範例:

1
2
3
4
await db.collection("user").insertOne({
account: account,
password: hashedPassword,
});

從上面的說明中我們可以知道,insertOne()接受兩個參數,第一個是要插入的文檔(document),第二個是一個選項對象(options),用於指定插入的一些選項。

因此,我們這段code的意思就是,到資料庫裡找到名為user的資料夾,在這個資料夾裡面插入一個文檔,文檔的標籤是**accountpassword**,相對應的內容是變數account裡的內容以及變數hashedPassword裡的內容。

這個範例裡比較需要注意的是要確認這個使用者是否已經創建過,因此這就需要用到我們下一篇要學習的findOne()。

MongoDB做 CRUD 之 Read

承接上一段,我們要怎麼確認使用者是否已經創建過?其實就是去資料庫收尋一遍看是否有重複的帳號。

那在mongoose 裡面我們有兩個語法可以使用:find()findOne()

db.collection.find(filter, projection)

  • 多個文檔find 用於返回符合指定查詢條件的所有文檔,通常返回一個游標(cursor),你可以遍歷以獲取所有匹配的文檔。
  • 參數
    • filter:指定查詢條件的對象,用於過濾出符合特定條件的文檔。如果未提供,將返回集合中的所有文檔。
    • projection:可選參數,用於指定要返回的字段。可以使用 1 表示要返回的字段,0 表示不返回的字段。
  • 返回值:返回符合查詢條件的所有文檔的cursor,你可以對游標進行遍歷以獲取所有匹配的文檔。

db.collection.findOne(filter, projection)

  • 單一文檔findOne 用於返回符合指定查詢條件的第一個文檔。
  • 參數
    • filter:指定查詢條件的對象,用於過濾出符合特定條件的文檔。如果未提供,將返回集合中的第一個文檔。
    • projection:可選參數,用於指定要返回的字段。可以使用 1 表示要返回的字段,0 表示不返回的字段。
  • 返回值:返回符合查詢條件的第一個文檔,如果未找到則返回 null

find()findOne()的差別在於:你是要全部符合條件的資料還是只要第一筆,但因為這個專案我們都只會有一筆符合條件的,因此用哪個語法都可以。

因此我們的code長這樣:

1
2
3
4
5
6
7
8
const existingUser = await db
.collection("user")
.findOne({ account: account });
if (existingUser) {
res.status(422).json({ message: "用戶已存在" });
client.close();
return;
}

這邊我們設立的條件就是這段:{ account: account }

當我們在資料庫找到標籤account裡的值跟我們傳進來的值是一樣的時候,它就會回傳符合指定查詢條件的第一個文檔,反之就會回傳null

之後就是判斷,因為我們需要的是資料庫裡沒有重複的account ,因此當回傳不是null時我們就會回傳錯誤訊息並且關閉連線並離開。

補充內容:

當使用 find 方法進行查詢時,你可以使用 MongoDB 的 Query Operators 來指定條件。

由於內容很多這邊貼上連結方便大家查閱:被迫吃芒果的前端工程師 - MongoDB CRUD 之 Read

MongoDB做 CRUD 之 Update

update跟insertion 一樣都有分成加入一筆跟加入多筆的語法,以及還有一個replaceOne

  1. db.collection.updateOne(filter, update, options) 更新符合特定條件的第一個文檔。
  2. db.collection.updateMany(filter, update, options) 更新符合特定條件的所有文檔。
  3. db.collection.replaceOne(filter, replacement, options) 替換符合特定條件的第一個文檔。

參數所代表的意思:

  • filter:指定更新的條件,找出符合條件的文檔(單一或多個)。
  • update:指定更新的內容,可以是更新操作符或新的文檔內容。
  • replacement:新的文檔內容,完全替換掉符合條件的文檔。
  • options:選擇性參數,用於控制更新的行為,例如 upsert(如果找不到符合條件的文檔是否新增)等。

這次的案例是使用這個專案裡的打卡系統的API來做範例:

1
2
3
// checkIn的結構:
numberId: numberId,
clockRecords: [],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const today = new Date().toLocaleDateString("zh-Tw");
const clockInTime = moment().format("LT");

const existingDate = await db
.collection("checkIn")
.findOne({ clockRecords: { $elemMatch: { date: today } } });

if (existingDate) {
await db.collection("checkIn").updateOne(
{ numberId: numberId, "clockRecords.date": today },
{
$set: {
"clockRecords.$.clockIn": clockInTime,
},
}
);
} else {
await db.collection("checkIn").updateOne(
{ numberId: numberId },
{
$push: {
clockRecords: {
date: today,
clockIn: clockInTime,
clockOut: "",
},
},
}
);
}

在更新這塊我使用的是updateOne()這個語法:

db.collection.updateOne(filter, update, options)

  • 參數:
    • filter:指定更新的條件,找出符合條件的第一個文檔。
    • update:指定更新的內容,可以是更新操作符或新的文檔內容。
    • options:選擇性參數,用於控制更新的行為,例如 upsert(如果找不到符合條件的文檔是否新增)等。

我們從最上面看下來,**findOne({ clockRecords: { $elemMatch: { date: today } } })**,這段有一個還沒介紹過的語法 $elemMatch ,在官方網站的解釋是:

它的意思是說陣列裡的檔案,至少要有一個元素滿足指定的查詢條件。這裡我設定的條件就是date這個標籤裡的值必須是要符合today裡的值。

因此當我的code執行到這行時,它會先去判斷我今天有打卡過了嗎?如果有那我的更新條件就會是{ numberId: numberId, "clockRecords.date": today } ,但如果沒有更新條件就會是{ numberId: numberId }

設定好更新條件後,我們就要來處理更新內容,這邊我用到兩個語法:$set**$push**

其實都蠻好理解的,第一個是將欄位的值替換為指定值。第二個是將指定的值加到陣列中。

另外我們看到這行:$set: {"clockRecords.$.clockIn": clockInTime,}

clockRecords.$.clockIn裡面的$這個符號的意思是:用於指定更新陣列中符合條件的元素。

所以整段話的意思是:只更新 clockRecords 陣列中符合"clockRecords.date": todayclockIn 字段。

那如果我們不加$這個符號的話,就會變成clockRecords 陣列中的所有的 clockIn 字段都會是 clockInTime

MongoDB做 CRUD 之 Delete

還是一樣,有分成刪除單一檔案跟多筆檔案的語法,這邊就不水字數了。

這應該是CRUD裡面最好理解的一個語法,我使用我稱之為one系列的語法:deleteOne()

db.collection.deleteOne()

  • 用途: 刪除符合指定條件的第一個文檔。

  • 語法:

    1
    2
    3
    4
    db.collection.deleteOne(
    <filter>, // 刪除條件,用來定位要刪除的文檔
    <options> // 選擇性選項,可以指定確定刪除的行為
    );

我們只需要把刪除條件設立好就可以了。

1
await db.collection("employees").deleteOne({ numberId: employeesId[0] })

結語

在還沒開始這個專案時,我把建構出API的一切想得非常簡單,結果現實狠狠的打了我一個臉。

因為完全沒有基礎導致常常碰壁,又加上這個專案我還用了TypeScript來做,在Next JS、mongoose、TypeScript這三個主要的工具都不熟悉的情況下,我整個進度可以說是一直的延後。

但好險最後還是有完成,接下來就是要來認真打磨客戶端的side project了!

參考資料:
https://www.mongodb.com/docs/

https://israynotarray.com/nodejs/20220416/2123631571/