消失了一個月,終於回來了~~雖然有點晚但祝大家新年快樂~~
整個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 | // db.tsx |
這個基本上就是我在這個專案所有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
5db.collection.insertMany([
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 35 },
{ name: 'Charlie', age: 40 }
]);
區別和適用場景:
- 性能:
insertOne
通常比insertMany
更快,因為它處理的文檔數量較少。 - 使用場景:
- 使用
insertOne
,當你只想插入單個文檔時。 - 使用
insertMany
,當你需要一次性插入多個文檔時,這樣可以減少與數據庫的通信次數,提高效率。
- 使用
案例:
我們用這次專案的建立使用者來做範例:
1 | await db.collection("user").insertOne({ |
從上面的說明中我們可以知道,insertOne()
接受兩個參數,第一個是要插入的文檔(document),第二個是一個選項對象(options),用於指定插入的一些選項。
因此,我們這段code的意思就是,到資料庫裡找到名為user的資料夾,在這個資料夾裡面插入一個文檔,文檔的標籤是**account
、password
**,相對應的內容是變數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 | const existingUser = await db |
這邊我們設立的條件就是這段:{ account: account }
當我們在資料庫找到標籤account裡的值跟我們傳進來的值是一樣的時候,它就會回傳符合指定查詢條件的第一個文檔,反之就會回傳null
。
之後就是判斷,因為我們需要的是資料庫裡沒有重複的account
,因此當回傳不是null
時我們就會回傳錯誤訊息並且關閉連線並離開。
補充內容:
當使用 find
方法進行查詢時,你可以使用 MongoDB 的 Query Operators 來指定條件。
由於內容很多這邊貼上連結方便大家查閱:被迫吃芒果的前端工程師 - MongoDB CRUD 之 Read
對MongoDB做 CRUD 之 Update
update跟insertion 一樣都有分成加入一筆跟加入多筆的語法,以及還有一個replaceOne
:
db.collection.updateOne(filter, update, options)
更新符合特定條件的第一個文檔。db.collection.updateMany(filter, update, options)
更新符合特定條件的所有文檔。db.collection.replaceOne(filter, replacement, options)
替換符合特定條件的第一個文檔。
參數所代表的意思:
filter
:指定更新的條件,找出符合條件的文檔(單一或多個)。update
:指定更新的內容,可以是更新操作符或新的文檔內容。replacement
:新的文檔內容,完全替換掉符合條件的文檔。options
:選擇性參數,用於控制更新的行為,例如upsert
(如果找不到符合條件的文檔是否新增)等。
這次的案例是使用這個專案裡的打卡系統的API來做範例:
1 | // checkIn的結構: |
1 | const today = new Date().toLocaleDateString("zh-Tw"); |
在更新這塊我使用的是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": today
的 clockIn
字段。
那如果我們不加$
這個符號的話,就會變成clockRecords
陣列中的所有的 clockIn
字段都會是 clockInTime
。
對MongoDB做 CRUD 之 Delete
還是一樣,有分成刪除單一檔案跟多筆檔案的語法,這邊就不水字數了。
這應該是CRUD裡面最好理解的一個語法,我使用我稱之為one系列的語法:deleteOne()
db.collection.deleteOne()
用途: 刪除符合指定條件的第一個文檔。
語法:
1
2
3
4db.collection.deleteOne(
<filter>, // 刪除條件,用來定位要刪除的文檔
<options> // 選擇性選項,可以指定確定刪除的行為
);
我們只需要把刪除條件設立好就可以了。
1 | await db.collection("employees").deleteOne({ numberId: employeesId[0] }) |
結語
在還沒開始這個專案時,我把建構出API的一切想得非常簡單,結果現實狠狠的打了我一個臉。
因為完全沒有基礎導致常常碰壁,又加上這個專案我還用了TypeScript來做,在Next JS、mongoose、TypeScript這三個主要的工具都不熟悉的情況下,我整個進度可以說是一直的延後。
但好險最後還是有完成,接下來就是要來認真打磨客戶端的side project了!