MySQL
資料庫管理系統 DBMS
- 資料庫管理系統 (database management system,縮寫: DBMS) 是一種為管理資料庫而設計的管理系統。具有代表性的資料管理系統有:Microsoft SQL Server、MongoDB、MySQL 及 PostgreSQL 等。
-簡單來說,DBMS就是管理資料庫的軟體。資料庫在概念上來說,可以被分成兩種:
1. Relational Database (or SQL Database):Relational Database 是一種存儲並提供對彼此相關的數據點的訪問的資料庫。
2. Non-Relational Database (or NoSQL Database)
增刪查改 CRUD
- 增刪查改( 英語,CRUD):增加 (Create,意為「建立」)、刪除 (Delete) 、(Read,意為「讀取」)、改正(Update,意為「更新」),是在 DBMS 當中,一連串常見的操作行為。
- 增删查改除了常用於SQL資料庫之外,也在與網站的 API 埠口時常使用。在 Restful API 的製作時,會再次使用。但網站的 API 埠口使用 HTTP 協定傳送通訊,所以原本的「增删查改」所對應的英文詞彙會因此而改名,而不再對應 CRUD。比如「查」不再是 Read,而改為 GET;「增」不再是 Create,而改為 POST;「改」不再是 Update,而改為 PUT 等等。
Keys
關係鍵 (keys) 是關聯式資料庫的重要組成部分。關係鍵是一個表中的一個或幾個屬性,用來標識該表的每一行或與另一個表產生聯絡。在 DBMS 當中,主要的 keys 有:
1. 主鍵 (英語:primary key) - 是資料庫表中對儲存資料物件予以唯一和完整標識的資料列或屬性的鍵。一筆資料只能有一個主鍵 (但可以由兩個以上的行組成 primary key ),且主鍵的取值不能缺失,即不能為空值(Null)。
2.外鍵 (Foreign Key) 是指向其他表格的主鍵的欄位,用於確定兩張表格的關聯性。
3. 自然鍵 (naturalkey) - 若使用在真實生活中唯一確定一個事物的標識,來當作資料庫的 primary key,則此 primary key 可被稱作是 natural key。例如:台灣的身份證字號可以當作資料庫的 natural key。
4. 代理鍵 (surrogate key) - 相對於 natural key,在當資料表格中的所有現有欄位都不適合當主鍵時,例如資料太長,或是意義層面太多,就會製作一個無意義的欄位來代為作主鍵。
5. 複合主鍵 (composite key) - 當資料表的主鍵 (Primary Key) 如果是由多個欄位組成,則稱為composite key。
SQL
- SQL (Structured Query Language,結構化查詢語言)是一種特定目的程式語言,用於對關聯式資料庫管理系統 (Relational DBMS,or RDBMS)下達指令。SQL 在 1987 年成為國際標準化組織 (ISO) 標準。
- 雖然有這一標準的存在,但大部分的 SQL 代碼在不同的資料庫系統中並不具有完全的跨平台性。也就是說,雖然 SQL 這門程式語言可以用來操作 DBMS,但每個 DBMS 所接受的 SQL 語法有些微差異。例如:用來操作 MySQL 這個 DBMS 的 SQL 程式碼不能全部拿去用來操作 Microsoft SQL Server 這個 DBMS。
Asynchronous JavaScript (Ajax)
- AJAX 即「Asynchronous JavaScript and XML」(非同步的 JavaScript 與 XML 技術),指的是一套綜合了多項技術的瀏覽器端網頁開發技術。
- AJAX 在客戶端使用各種 Web 技術來創建異步 (asynchronous) Web 應用程序。應用程序可以在背景從服務器發送和獲得數據,而不干擾現有頁面的顯示和行為。通過將數據交換層與表示層分離,Ajax 允許網頁以及擴展的 Web 應用程序動態地更改內容,而無需重新加載整個頁面。在實踐中,數據的傳送通常使用 JSON 而不是 XML。
- 常見的 Ajax 應用的例子是,我們在 YouTube 或是 Google 搜尋時,網站會根據我們前面打的幾個字,猜想我們想要搜尋的關鍵字是什麼。這就是不干擾現有頁面的顯示和行為的情況下,從服務器發送和獲得數據,並且更新網頁的方法。
同步與異步
- 通常來說,JavaScript 的特性是 single-threaded synchronous,代表 JavaScript 是個一次只會做一件事情的程式語言。然而,JS 有內建的 asynchronousfunction,例如 setTimeout()。setTimeout() funtion 設置一個計時器,一旦計時器時間到,該計時器就會執行一個函數或指定的一段代碼。 setTimeout() 的語法為:
setTimeout(code, delay)
- Code 是 delay 結束時要執行的程式碼,delay 是在執行指定的函數或代碼之前計時器應等待的時間 (以毫秒為單位) 如果省略此參數,則使用值 0,表示“立即”執行。
範例:
console.log('start');
setTimeout(() => {
console.log('Here is the code."');
}, 2000)
console.log(end');
上面這段程式碼執行的結果是:
start
end
Here is the code.
Promise
- Promise 是現代 JavaScript 中異步編程的基礎。Promise 是一個由 asynchronous function 所 return 的物件,Promise 主要功能是會代理一個建立時不用預先得知結果的值。
- Promise 使我們能夠連結發動非同步操作後,最終的成功值 (success value) 或失敗訊息 (failure reason )的處理函式 (handlers)。
- 我們向伺服器傳送 request 之後,因為需要等待 response 的時間,所以我們會先得到一個目前的狀態是「擱置」(pending) 的 Promise。
一個 Promise 物件有可能會處於以下三種狀態:
1. 擱置 (pending):初始狀態,並不是 fulfilled 與 rejected。
2. 實現 (fulfilled):表示操作成功地完成。
3. 拒絕 (rejected):表示操作失敗了。
- Promise 在pending 後的幾秒之內,狀態可能變成 fulfilled 或是rejected。一個處於擱置 (pending) 狀態的 Promise,若操作成功,能夠將狀態變成 fulfilled 或是因為某些原因或錯誤而被操作失敗,變成拒絕(rejected) 狀態。當上述任一狀態轉換發生時,那些透過 then 方法所繫結的 callback 就會被調用。
promise.then(callbackFun);
promise.catch(callbackFun);
範例:
let promiseObject = fetch(URL);
promiseObject.then((data) => { W
console.log(data);
})
這段程式碼中,當 promiseObject 從 pending 變成 fulfilled之後,.then() 內部的 callback function 就會被 JavaScript 自動執行。執行時,帶入的參數就是從 URL 獲得的 HTTP Response 內容。
fetch:JavaScript's Fetch API allows us to send HTTP requests. (asynchronous function)
運作流程圖 1:
運作流程圖 2:
範例:獲取網站 data
let promiseObject = fetch(URL);
promiseObject.then((data) => { W
console.log(data);
})
寫法 1:
fetchPromise.then((response) => {
response.json().then((data) => {
console.log(data);
});
寫法 2:
fetchPromise
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
});
寫法 3:
fetchPromise
.then((response) => response.json()) // callbackFun return response.json()
.then((data) => {
console.log(data);
});
這段程式碼中,response.json() 將 response 的資料轉成 JSON 格式;.json() method is also a asynchronous function and will return a promise object
結果如下:
fetch()
- fetch()本身是 JavaScript提供,可以寄送 HTTP request 的方法。fetch() 會 return一個 promise,而這個 promise 在 HTTP response 被接收到時,會從 pending 轉變成 fulfilled。
- 另外,如果對 fetch() 所 return 的 promise 做 .then(callback) 時,JavaScript 自動帶入 callback 的參數,會是一個「Response Object」。這個 Response Object 代表 HTTP response。
- 如果遇到 404 的情況,fetch() 的 promise 並不會出現 rejected 狀態,而是會變成 fulfilled 狀態。但我們可以用使用 Response Object 的屬性 status 或 ok 來確定我們得到的 Response 是200 OK 還是 404 Not Found。
什麼樣的情況下,fetch() Promise 才會必成 rejected 呢?
只有當網路故障或有任何原因阻止請求完成時,該 Promise 才會變成 rejected。
例如:當我們的網址是完全亂打時,因為沒有相對應的伺服器可以回傳 HTTP Response,所以會產生「TypeError: NetworkError when attempting to fetch resource.」由此可知,這裡發生了網路錯誤,導致 Promise 就變成 rejected 狀態。
.json()
- .json() 是 JavaScript 內部的 Response Object 的可用 method。
- .json() 方法接讀取 Response Object 直到完成。此 method 會 return 一個 Promise,而且該 Promise 會將 body tex t解析為 JSON。所以,我們使用 .json() 就可以將 fetch(URL) 所回傳的 Response Object內部的文本資料取出。
Catching Errors
- Promise 物件提供了一個 catch() 方法來進行錯誤處理,跟 then() 很像。
- .catch() 被調用時,會傳入一個 callback function 當作參數。傳遞給 catch() 處理函數在異步操作失敗時自動被 JavaScript 調用。catch() 內部的 callback function 被調用時,參數會被放入錯誤訊息,通常以變數 e 或是 err 代表錯誤 (error)。
- 當串連多個 .then() 語句時,後一個.then() 內部的 callback function 被執行時,所用的參數是前一個 .then() 中的 callback function 所回傳的值。如果將 catch() 添加到 Promise Chain 的末尾,那麼當任何異步函數調用失敗時都會調用它。
範例:
let promiseObject = fetch(URL);
promiseObject.then((data) => { W
console.log(data);
})
fetchPromise
.then((response) => response.json()) // callbackFun return response.json()
.then((data) => {
console.log(data);
})
.catch((e) => {
console.log(e);
});
將 catch() 添加到 Promise Chain 的末尾,那麼當任何異步函數調用失敗時都會調用它。
Combining Multiple Promises
Promise.all()
- 當我們的操作由多個異步函數組成時,我們需要用到 promise chaining,讓我們在開始下一個函數之前完成前一個函數。這種情況下,每個 Promise 都互相依賴。
- 有時,我們需要所有 Promise 都被 fulfilled,但它們並不相互依賴。在這種情況下,將它們全部一起啟動,然後在它們全部 fulfilled 時收到通知會更有效。JavaScript當中,提供了 Promise.all() 這個 static method,它接受一個 promise array 並返回一個 promise。
Promise.all() 返回的 promise 是:
• fulfilled - 如果所有在 array 當中的 promises 都變成 fulfilled,則 Promise.all() 所 return 的 promise 狀態會變成 fulfilled。.then() 被 JavaScript 調用時,參數是 array of responses,順序跟 Promise.all() 參數的 array of promises 的順序相同。
• rejected-當任一個 array 當中的 promises 變成 rejected,則 Promise.all() 所 return 的 promise 狀態會變成 rejected。此時,.catch() 被 JavaScript 調用時,參數會是被 rejected 的 promises 的錯誤訊息。
範例:
const fetchPromise1 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
);
const fetchPromise2 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found"
);
const fetchPromise3 = fetch(
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json"
);
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
responses.forEach((response) => {
console.log(response.url, response.status);
});
})
.catch((e) => {
console.log(e);
});
結果如下:
Note:status 404 並非 error message。如果將其中之一個 URL 輸入錯誤網址,則會報錯:TypeError: Failed to fetch
Promise.any()
有時,我們可能需要履行一組 Promise 中的任何一個,而不關心哪一個,那我們可以用 Promise.any()。只要 Promise array 中的任何一個變成 fulfilled,就執行. then(),或者如果所有 promises 都被拒絕,則執行 .catch()。
Async and Await 關鍵字
Async 關鍵字為我們提供了一種更簡單的方式來處理基於 async promise 的代碼:
async function myFunction() {
}
// This is an async function
}
在asynchronous function 中,你可以在調用會 return promise 的函數 (asynchronous function) 之前使用
await 關鍵字。這使得代碼在該點等待直到 Promise 被 fulfilled 或是 rejected。await 關鍵字只能放在asynchronous function 內部!
特別注意!JavaScript 設定所有的 async function 都一定會 return 一個 Promise Object,不論我們在async function 內 return 什麼值,在 async function 內部 return 的任何值,在 async function 所 return 的 Promise 變成 fulfilled 時,執行 .then() 的 callback function 內部自動變成參數。例如:
async function myFunction() {
return 10;
}
let promise = myFunction();
promise.then(data => {console.log(data);})
在最後一行的data會是10。
範例:
async function myFunc() {
return 100;
}
let result = myFunc();
console.log(result); // Promise Object
result.then((data) => console.log(data)); // 100
結果如下:
特別注意!若程式碼是:
async function fetchSomething() {
const response = await fetch(URL);
}
在這裡,我們調用了 await fetch(),response 並不會是一個Promise!使用了 await 關鍵字,我們會獲得 URL 回應的完整的 Response Object,就像 fetch() 是一個同步函數 (synchronous) 一樣!
範例:
async function fetchProduct() {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
);
console.log(response); // 因為使用了 await 關鍵字,我們會獲得 URL 回應的完整的 Response Object,此時 response 已經不是 promise [不需要再用 .then() 的語法]
}
fetchProduct();
結果如下:
接續上例,獲取 URL 裡面資料:
async function fetchProduct() {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
);
const data = await response.json();
console.log(data);
}
fetchProduct();
結果如下:
我們在 asynchronous function 內部甚至可以使用 try...catch 進行錯誤處理,就像代碼是同步的一樣。
範例:使用 try...catch 進行錯誤處理
async function fetchProduct() {
try {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
);
const data = await response.json();
console.log(data);
} catch (e) {
console.log(e);
}
}
fetchProduct();
當有錯誤發生時就會報錯!例如下圖:
Node.js Event Loop
- 在 Node.js 當中,將凡事有任何需要等待結果的、請求外部資源才能進行的函式,都會被放到 Event Loop 中等待。
- 當運算結果出來了或是資源載入完成後,這些正在等待被執行的函式,都會被 Node.js 依序執行。如此一來,Node.js 可以保持忙碌且維持高效率。
- Node.js 的 Event Loop 與瀏覽器的 Event Loop 不盡相同。Event Loop 的結果也跟 JavaScript 引擎的版本有關。
Queue
在認識 Event Loop之前,先來認識一種資料結構 - Queue。Queue 與 Stack 是兩個相似但原則相反的資料結構。Queue 是一種列隊式的結構,採用先進先出 (First In First Out, FIFO) 為原則。 Stack 則是堆狀的結構,採用後進先出為 (Last In First Out, LIFO) 原則。
在 Node.js 的 Event Loop 當中,大致可分成以下幾種 Queue:
1. 優先級別:nextTick queue 以及 microTask Queue。
2. 普通級別:macrotask queue (或叫做 task queue)。其中,macrotask queue 又有 timers, pending callbacks, Idle, prepare, polling, check, and close callbacks 這六種。
• nextTick Queue - 優先程度最高的 queue。給定的 process.nextTick(callbackFn) 的 callbackFn 都會被放入這個 queue 內部。
• microTask Queue - 優先程度第二高的 queue。當 promise object 的狀態,由 pending 轉變為 fulfilled 或 rejected 時,.then(callbackFn).catch(callbackFn) 所執行的 callbackFn 會被排在這個 queue。
以下的都是 macroTask queue:
▪ timers - 當 setTimeout(callbackFn) 跟 setInterval(callbackFn) 所設定的時間倒數完畢時, callbackFn 會被放來這裡等待執行。
▪Pending callbacks - 給作業系統做使用的 queue,例如:socket連線時的錯誤,或是傳輸控制協定層出現錯誤,相關的 callback functions 會被放到這邊來。
▪ Idle, prepare - 給 Node.js 內部做使用的 queue。
▪Polling - 當 I/O 有 callback function 時使用的 queue。例如:.on(data”, callbackFn) 當中 callbackFn 就會被放入 polling。
▪Check - 給 setImmediate() 的 callback functions 使用的 queue。
▪Close Callbacks - 當 socket 或是檔案被關閉或是突然中斷連線時,使用的關閉動作 callback functions 會被放在這裡。
Node.js 運行程式碼的順序
1. 將整份程式碼先掃描一次,若遇到同步函式,就馬上執行。
2. 若遇到異步函式,則將 callback function 分配到各自歸屬的 queue 內部。例如:setImmediate() 的callback function 就會被放到 Check。
3. 當整份程式碼完成掃描後,Node.js 會重複 event loop。只要 queue 還有 callback 尚未被觸發,Node.js就會一直循環,不斷循環下去。例如:setTimeout() 有 callback function,但需要幾秒後才觸發,那這之間的時間 event loop 就會不斷循環。當然,這中間的幾秒也有可能有其他的 callback functions 被放入 queue。
Promise Based API
API (Application Programming Interface) 的中文是應用程式介面。Application 是指任何具有功能的程式,Interface (接口) 可以被認為是兩個程式之間的服務契約。該合約定義了兩個程式之間如何相互通信。例如:當程式甲需要程式乙幫他做某件事,或是取得某些資料的時候,程式乙會定義一套的標準或接口,告訴任何想要程式乙提供服務的對象,如何跟程式乙溝通。這套標準就是 API。
這時程式甲並不需要知道程式乙做了什麼,怎麼做的。程式甲只需要知道三件事:
1. API 上面要求要提供什麼資料,才能向程式乙溝通?
2. 成功的話,程式乙會回復給我什麼?
3. 失敗的話,程式乙會回復給我什麼?
API 上面會把這些情況寫得明明白白。
製作 API
若我們想要製作一個 API,而 API 中的 function 會 return promise object,使得調用這些function 時,可以使用 .then(),.catch() 等語法,那我們就必須使用 Promise class 的 constructor。 Promise constructor 接受一個函數作為參數。我們將這個函數稱為 executor。
executor 函數本身有兩個參數,它們都是函數,通常稱為 resolve 和 reject 如果異步函數成功,則調用 resolve,如果失敗,則調用 reject。 Resolve 以及 Reject 這兩個函數的 argument 只有一個,並且可以是任何的 data type。
範例:
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="text" id="name" />
<input type="text" id="delay" />
<button id="set-alarm">設定鬧鐘</button>
<div id="output"></div>
<script src="./app.js"></script>
</body>
</html>
app.js
const name = document.querySelector("#name");
const delay = document.querySelector("#delay");
const button = document.querySelector("#set-alarm");
const output = document.querySelector("#output");
// return Promise object
// pending的delay秒 => fulfilled
// 若delay < 0 => rejected
// Promise-based API
function alarm(person, delay) {
return new Promise((resolve, reject) => {
if (delay < 0) {
reject("delay不能小於0");
} else {
setTimeout(() => {
resolve(person + "起床!!");
}, delay);
}
});
}
button.addEventListener("click", async () => {
try {
let result = await alarm(name.value, delay.value);
output.innerHTML = result;
} catch (e) {
output.innerHTML = e;
}
});
執行結果:輸入姓名、秒數(毫秒、大於 0 ),則會顯示 resolve 的結果。若輸入秒數為負數,則會顯示 reject 的結果。
若輸入秒數為負數,則會顯示 reject 的結果。
連接到外部API
示範連結至 JokeAPI 網站,獲取笑話:
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="new-joke">獲取笑話</button>
<div id="output"></div>
<script src="./app.js"></script>
</body>
</html>
app.js
let button = document.querySelector("#new-joke");
button.addEventListener("click", () => {
joke();
});
let output = document.querySelector("#output");
let jokeUrl = "https://v2.jokeapi.dev/joke/Any?type=single";
async function joke() {
try {
let result = await fetch(`${jokeUrl}`);
let data = await result.json();
output.innerText += data.joke + "\n" + "\n";
} catch (e) {
console.log(e);
}
}
joke();
執行結果:每點擊一次按鈕,則會新增一則笑話。
MongoDB
JSON and BSON
JavaScript Object Notation,通常稱為 JSON,在 2000 年代初期由 JavaScript 創建者Douglas Crockford 定義為 JavaScript 語言的一部分。JavaScript 物件是簡單的容器,其中
一個 String key 可以映射到一個 value (這個 value 可以是數字、String,甚至是另一個件)。這種簡單的語言特性允許 JavaScript 物件在文件中非常簡單地表示:
JSON 的普遍性使其成為 MongoDB 在開發時的數據結構第一選擇。 但是,以下有幾個問題使 JSON 不太適合在數據庫內部使用:
1. JSON 是基於純文字的格式,而純文字在解析上很緩慢。
2. 另一個問題是,JSON 的高可讀性並無法節省儲存空間。
3. JSON 僅支持有限數量的基本 data types。
為了使 MongoDB 提高性能,因而發明了BSON 來解決以上的問題。BSON 基於 JSON,且具有高性能和通用性。
BSON 代表 Binary JSON,BSON 的二進制結構對 data types 和長度信息進行編碼,從而可以更快地對其進行解析,針對速度、空間和靈活性進行了優化。
例如,將 JSON 的 {“hello”: “world”} 換成 BSON 會得到:
\x16\x00\x00\x00\x02 hello\x00\x00\x00\x00\x00world\x00\x00
MongoDB Shell (mongosh)
MongoDB Shell (mongosh) 是一個功能齊全的 JavaScript 和 Node.js 16.x REPL (Read, Evaluate, Print, Loop) 環境,用於與 MongoDB 部署進行交互運作。我們可以使用 MongoDB Shell 直接用數據庫測試查詢和操作。
在 MongoDB 當中,我們可以一次擁有數個 databases 。每個 database 內部可以有數個collections。 Collections 等同於是 MySQL 當中的一個表格。
MongoDB Shell 常用的指令
• show dbs - 展示所有的資料庫。
• db - 展示目前所在的資料庫。
• use <db> - 將當前所在的資料庫切換到 <db>。若 <db>不存在,則製作出並且切換 <db>。
• show collections - 打印當前所在的資料庫的所有 collections。
在 MongoDB 中,document 指的是數據的基本單元或基本構建塊 (一筆資料)。
MongoDB Shell 當中,跟 CRUD 有關的常見語法
• db.collection.insertOne(<document>) - 參數為一個物件,功能為在 collection 當中新增一個 document。
新增第一筆資料:
• db.collection.insertMany([ <document 1>, <document 2>,...) - 參數為一個由物件組成的 array,功能為在 collection 當中新增一個或一個以上的 document。
新增其它二筆資料:
• db.collection.insert( <document or array of documents>) - 參數為一個物件或是一個由物件組成的 array,功能為在 collection 當中新增一個或一個以上的 document。
• db.collection.find(<query>) - 找尋 collection 中的資料。Query 的 data type 是 object,用來過濾找尋的資料。若想要獲得 collection 中的所有資料,query 可 以是 empty object,或者執行 find() 時不給定任何 argument 即可。
Comparison Query Operators:=> 參考資料
找尋資料裡面 age < 30 的資料: $lt
• db.collection.updateOne( <filter>, <update>) - 更新 collection 中第一筆找到的資料。Filter 的 data type 是 object,是指更新的選擇標準,與 find() 中 query 功能一模一樣。Update 的data type 也是 object,我們可以將被修改資料的新數據放在 update 這個位置。
使用:$set:
將原先 name: "Spance Kwan" 改成 name:"Spancer Kwan"
將原先 age: 35 改成 age: 36
使用:$set:
將原先 age: 36 改成 age: 37
使用:$currentDate:{lastModified: true}
新增一筆資料更新的時間記錄:lastModified: ISODate('2024-03-19T09:52:38.014Z')
• db.collection.updateMany(<filter>,<update>) - 功能也是更新 collection 中的資料,但可以一次性的更新 collection 中所有符合 filter 的多筆資料。
使用:$set:
將所有 major 為 "Computer Science" 的資料,改成 "CS"
• db.collection.deleteOne(<filter>) - 可以刪除 collection 內的第一筆符合 filter 的資料。
• db.collection.deleteMany(<filter>) - 刪除 collection filter 內所有符合 filter 的資料。
Mongoose
若要在程式語言中使用或存取 MongoDB,我們需要工具讓資料庫可以跟 JavaScript 程式碼連結。這類工具的特點就是,能夠將 JavaScript 中的 Object 轉換成 MongoDB 當中的 document,因此,這類的工具叫做 object-document mapping (ODM)。mongoose 是目前眾多 MongoDB 的 ODM 當中最熱門的。
使用 ODM 的好處:
1. 資料庫的結構能被追蹤。通常資料庫的結構經過改變之後,很難退回到未改變的結構。使用ODM 可以將資料庫的結構寫在程式碼內部,方便追蹤與更改。
2. 通常 ORM/ODM 會內建保護機制或是保護型語法,所以使用 SQL 資料庫時,就不用擔心 SQL Injection 之類的攻擊。
*. SQL 資料庫使用的工具叫做 ORM,而 NoSQL 資料庫使用的工具叫做 ODM。兩者功能相同但名稱不同。
3. 讓 Project 更符合 MVC 模型。Mongoose 是 model,用來與 MongoDB 互動獲得或改變資料、View 是 EJS,Controller 則是 app.js 來擔任。
Mongoose 套件下載
working director:mongoose practice
1. npm init
2. npm express ejs
3. npm install mongoose
使用 mongoose 連接 MongoDB:
app.js
const express = require("express");
const app = express();
const mongoose = require("mongoose");
app.set("view enginr", "ejs");
//連接 mongoDB
mongoose
.connect("mongodb://localhost:27017/exampleDB")
.then(() => {
console.log("成功連結mongoDB....");
})
.catch((e) => {
console.log(e);
});
app.listen(3000, () => {
console.log("伺服器正在聆聽port 3000....");
});
Model and Schema
在 Mongoose 中,兩個 keyword 需要記得:
1. Schema - 每個 Schema 映射到一個 MongoDB 中的 Collection,並且定義該 Collection 中document 的架構,包含默認值、最大長度、最大值、最小值等等。
2. Model - 包裝 Schema 的容器。在數據庫中,Schema 所對應到的 Collection 提供了一個接口,可以用 Model 來對 Collection 進行新增、查詢、更新、刪除記錄等功能。
Model 就像是 SQL 當中的 table,而 Schema 是 create table 的步驟。
Schema 的語法為:
import mongoose from 'mongoose';
const {Schema} = mongoose;
const blogSchema = new Schema({
title: String, // String is shorthand for {type: String},
date: {type: Date, default: Date.now},
meta: {votes: Number, favs: Number},
})
在 blogSchema 的 constructor 當中,參數為一個物件,而物件的每個 key 都定義了 blog collection 當中的 document 的屬性。並且在物件的每個 key 賦予的 value 為一個屬性,為SchemaType 的物件。常見的 SchemaType 有:String, Number, Date, Boolean, ObjectId, Array, Decimal128, Map 等等。
範例:
const { Schema } = mongoose;
const studentSchema = new Schema({
name: String,
age: Number,
major: String,
scholarship: {
merit: Number,
other: Number,
},
});
Model 的語法為:
const Blog = mongoose.model('Blog', blogSchema);
特別注意,mongoose.model() 的第一個參數是 String,代表 collection 的名稱。這裡使用的 String 必須為大寫英文字母開頭,且為單數形式。例如:如果我們希望製作名為 students 的collection,就必須使用 'Student',而如果想要製作名為 people 的 collection,就必須用 'Person'。(Mongoose 會自動轉換,我們需要確保提供正確的拼字即可)
範例:
const Student = mongoose.model("Student", studentSchema);
const newObject = new Student({
name: "Esther",
age: 27,
major: "Mathematics",
scholarship: { merit: 6000, other: 7000 },
});
在 Mongoose 中,跟 CRUD 有關的常見操作:
• document.save() - 在 MongoDB 中儲存 document, returns a promise
doc.save().then(savedDoc => { savedDoc === doc; // true });
範例:
newObject
.save()
.then((saveObject) => {
console.log("資料已經儲存完畢,儲存的資料是: ");
console.log(saveObject);
})
.catch((e) => {
console.log(e);
});
執行 node app.js,結果為:
伺服器正在聆聽port 3000....
成功連結mongoDB....
資料已經儲存完畢,儲存的資料是:
{
name: 'Esther',
age: 27,
major: 'Mathematics',
scholarship: { merit: 6000, other: 7000 },
_id: new ObjectId('65fa8ff9df4eed0ff4f18948'),
__v: 0
}
在 Mongoose 當中,許多 methods 的 return type 都是「Query」, Query 是一種 Mongoose Class (根據 documentation,Query 是一種 thenable object [可以使用.then()],但不是Promise),提供用於 find、update 和 delete documents 等操作提供 method chaining 。如果要讓這些 methods 的 return type 變成 promise,可以讓 Query 執行 .exec() 即可。
• Model.find(filter) - 找到所有符合 filter 條件的物件(array of documents)。參數是一個物件,用來提供過濾尋找的條件。
範例 1:查找所有資料
寫法一:
const Student = mongoose.model("Student", studentSchema);
Student.find({})
.exec()
.then((data) => {
console.log(data);
})
.catch((e) => {
console.log(e);
});
寫法二:將找尋到的資料以網頁呈現
app.get("/", async (req, res) => {
try {
let data = await Student.find().exec();
res.send(data);
} catch (e) {
console.log(e);
}
});
執行 node app.js,開啟 locallhost:3000:
範例 2:查找特定條件的資料
查找所有 scholarship 裡面 merit 值大於 1500 的資料
const Student = mongoose.model("Student", studentSchema);
Student.find({ "scholarship.merit": { $gte: 1500 } })
.then((data) => {
console.log(data);
})
.catch((e) => {
console.log(e);
});
執行 node app.js:
[
{
scholarship: { merit: 3000, other: 1500 },
_id: new ObjectId('65f95c6fafc22b0ab78bf205'),
name: 'Grace Xie',
age: 27,
major: 'CS'
},
{
scholarship: { merit: 6000, other: 7000 },
_id: new ObjectId('65fa8ff9df4eed0ff4f18948'),
name: 'Esther Lam',
age: 27,
major: 'Mathematics',
__v: 0
}
]
• Model.findOne(filter) - 找到第一個符合 filter 條件的物件 (a document)。參數是一個物件,用來提供過濾尋找的條件。
範例:
app.get("/", async (req, res) => {
try {
let data = await Student.findOne({name:"Grace"}).exec();
res.send(data);
} catch (e) {
console.log(e);
}
});
執行 node app.js,開啟 locallhost:3000:
關於 Query Object 與 Promise 的比較
Query Object 以及 Promise 兩者非常相像:
1. Query Object 本身是一種 thenable object,代表後面可以串接 .then() 以及 .catch()。在上述的 find() 以及 findOne() 兩個 method 的 return 值都是 Query Object。因此,如果將上述範例中的 .exec() 全部刪掉,會發現程式碼還是能夠照常運作的。
2. Promise 語法也是可以使用 .then() 以及 .catch()。
這裡可以看出,在 Query Object 後面加上 .exec(),轉變成Promise,這個步驟,似乎不是必要的。那兩者有何不同?或者,哪些情況該用哪個呢?
答案是,不管在哪種情況,在 Query Object 後面加上 .exec(),讓它變成 Promise 都是比較好的。原因在於,使用 Promise 的話,JavaScript 的 try... catch... 語法中,catch 可以顯示更好的錯誤追蹤訊息。詳細的例子可以參考 mongoose 的 documentation。使用 Promise 的話,錯誤追蹤訊息會顯示出問題的 .exec() 是在哪一行程式碼。因此,加上 .exec() 會比不加來得更好。
• Model.updateOne (filter, update, options) - 找到第一個符合 filter 條件的物件,並且將資料更新 update 的值。 filter, update 這兩個 parameter 的資料類型都是 object。.then() 內部的 callback 被執行時,帶入的 parameter 是更新操作的訊息。例如:acknowledged, modifiedCount, upsertedId 等等。 Options 物件可設定 runValidators (執行驗證器),若 update 物件的值不符合 Schema 的設定,則出現 error。
範例:
const Student = mongoose.model("Student", studentSchema);
Student.updateOne({ name: "Esther" }, { name: "Esther Lam" })
.exec()
.then((msg) => {
console.log(msg);
})
.catch((e) => {
console.log(e);
});
執行 node app.js,會獲得更新操作的訊息:
{acknowledged: true,modifiedCount: 1,upsertedId: null,upsertedCount: 0,matchedCount: 1}
資料裡面 name: "Esther" 也已經被修改成 name: "Esther Lam":
1. 在 mongosh terminal 裡面查找資料是否已被更新:
db.students.find()
2. 在 app.js 裡面查找資料是否已被更新:
Student.find({})
.exec()
.then((data) => {
console.log(data);
})
.catch((e) => {
console.log(e);
});
• Model.updateMany(filter, update, options) - 找到所有符合 filter 條件的物件,並且將符合 filter 的每一筆資料,更新 update 的值。 filter, update 這兩個 parameter 的資料類型都是object。 .then() 內部的 callback 被執行時,帶入的 parameter 也是更新操作訊息。Options 可設定 runValidators。
• Model.findOneAndUpdate(condition, update, options) - 找到第一個符合 condition 條件的物件,並且更新 update 的值。 condition, update, options 這三個 parameter 的資料類型都是 object。.then() 內部的 callback 被執行時,若在 options 內部有表明 new 屬性為 true 則 .then() 內部的 callback 被執行時,帶入的 parameter 會是更新完成了 document。反之,沒有表明 new 是true,或設定 new 是 false (這是預設值),則 callback 的 parameter 會是更新前的 document。Option中也可以設定 runValidators。
範例:
const Student = mongoose.model("Student", studentSchema);
Student.findOneAndUpdate(
{ name: "Grace" },
{ name: "Grace Xie" },
{ runValidators: true, new: true }
)
.exec()
.then((newData) => {
console.log(newData); // 帶入的 parameter 會是更新完成了的資料 (document)
})
.catch((e) => {
console.log(e);
});
執行 node app.js,會獲得更新操作的訊息:
{
scholarship: { merit: 3000, other: 1500 },
_id: new ObjectId('65f95c6fafc22b0ab78bf205'),
name: 'Grace Xie', // 更新完成了的資料 (document)
age: 27,
major: 'CS'
}
updateOne() 與 findOneAndUpdate() 的使用時機為何?
updateOne() 是在當我們不需要更新後的 document 時 (之後再去尋找更新後的資料),並且希望節省一點資料庫操作時間和通訊流量的話,可以用的選擇。
但其他情況下,findOneAndUpdate() 提供更新完成的 document 是非常實用的功能 (表明 new 屬性為 true,即時得到更新後的資料)。
• Model.deleteOne(conditions) - 從 Collections 中刪除與 conditions 匹配的第一個 document。此 method 會 return 一個具有 deletedCount 屬性的 object。
範例:
const Student = mongoose.model("Student", studentSchema);
Student.deleteOne({ name: "Grace Xie" })
.exec()
.then((msg) => {
console.log(msg);
})
.catch((e) => {
console.log(e);
});
1. 執行 node app.js,結果為:
{ acknowledged: true, deletedCount: 1 }
2. 在 mongosh terminal 執行:db.students.find(),可以發現一筆資料已經被刪除了。
• Model.deleteMany(conditions) - 從 Collections 中刪除與 conditions 匹配的所有 documents。此 method 會 return 一個具有 deletedCount 屬性的 object。
其他所有的操作,可以參考 Mongoose CRUD 頁面。
Schema Validators
如果我們希望 Collections 中的資料,在被存放到 Collections 之前,可以經過驗證 (例如:員工資料庫的薪資欄位不能小於0),則可以在 Schema 中設定每個屬性的驗證器 (validators) 來達到此功能。
通常來說,Schema 屬性的設定語法是:name: String,但也可以寫成是:name: {type: String} 。因為 validator 本身是 Schema 屬性設定時,物件的一個屬性,所以加入validator 的語法會變成:
name:{
type: String,
required: true
}
因為每種 data type 所通用的驗證器不同,所以我們需要將每種驗證器歸類到各自的 data type上。
對於所有的 data type 都適用的驗證器有:
1. required −可放入一個 boolean 值,或是一個 array (包含一個值以及一個客製化的錯誤訊息),或是一個function。
範例一:
const studentSchema = new Schema({
name: {type:String, require: true},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {type:String, required: [true, "每個學生都需要選至少一個主修"]},
scholarship: {
merit: Number,
other: Number,
},
});
範例二:
const studentSchema = new Schema({
name: {type:String, require: true},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
},
},
scholarship: {
merit: Number,
other: Number,
},
});
2. default - 可設定屬性的預設值。
範例:
const studentSchema = new Schema({
name: {type:String, require: true},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
});
跟 String 有關的驗證器有:
1. uppercase (boolean)
2. lowercase (boolean)
3. enum (array of strings)
4. minlength (number)
5. maxlength (number)
跟 number 有關的驗證器有:
1. min
2. max
3. enum
範例:
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25}, // 最長輸入不得超過 25 字
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
});
Instance Method
在 Mongoose Model 當中的每筆資料都叫做 document,而 document 又叫做 instance。若我們希望某個 model 中的所有 documents 都可以使用某個 method,則可以將此 method 定義在 Schema 上。像這樣定義在 Schema 上的 method 就稱為 instance method 。
Instance method 的語法有兩種。第一種是在 Schema 內設定 methods 屬性並且給予一個物件,物件內部有 methods:
const animalSchema = new Schema(setting, {
methods: {
findSimilar Types (cb) {
return mongoose.model('Animal').find({ type: this.type}, cb);
}
}
});
範例:
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
{
methods: {
printTotalScholarship() {
return this.scholarship.merit + this.scholarship.other;
},
},
}
);
const Student = mongoose.model("Student", studentSchema);
Student.find({})
.exec()
.then((arr) => {
arr.forEach((student) => {
console.log(
student.name + "的總獎學金金額是" + student.printTotalScholarship()
);
});
});
執行 node app.js:
Spancer Kwan的總獎學金金額是0
Esther Lam的總獎學金金額是13000
或是,第二種方式是,我們可以直接設定 Schema 的 methods 屬性:
animalSchema.methods.findSimilar Types=function(cb) {
return mongoose.model('Animal').find({ type: this.type}, cb);
};
範例:
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
}
);
studentSchema.methods.printTotalScholarship = function () {
return this.scholarship.merit + this.scholarship.other;
};
const Student = mongoose.model("Student", studentSchema);
Student.find({})
.exec()
.then((arr) => {
arr.forEach((student) => {
console.log(
student.name + "的總獎學金金額是" + student.printTotalScholarship()
);
});
});
執行 node app.js:
Spancer Kwan的總獎學金金額是0
Esther Lam的總獎學金金額是13000
Static method
如果我們想要定義某個專屬於 Schema 使用的 method,則可以使用 static method。Static method 屬於 Schema本身,而不屬於 Mongoose Model 內部的 documents。此概念來自於物件導向程式設計。Static methods的設置方式有以下三種:
第一種方法:
const animalSchema = new Schema(setting, {
statics: {
findByName(name) {
return this.find({ name: new RegExp(name, 'i') });
}
}
});
範例:
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
{
statics: {
findAllMajorStudents(major) {
return this.find({major: major}).exec();
},
},
}
);
const Student = mongoose.model("Student", studentSchema);
Student.findAllMajorStudents("CS")
.then((data) => {
console.log(data),
})
.catch((e) => {
console.log(e);
});
執行 node app.js:(會找到所有 major: "CS" 的資料)
{
scholarship: { merit: 0, other: 0 },
_id: new ObjectId('65f911e5afc22b0ab78bf204'),
name: 'Spancer Kwan',
age: 37,
major: 'CS',
sholarship: { merit: 3000, other: 2000 },
lastModified: 2024-03-19T09:52:38.014Z
}
第二種方法,直接將 methods 設定在 Schema 的s tatics 屬性上:
animalSchema.statics.findByName = function(name) {
return this.find({ name: new RegExp(name, 'i')});
};
範例:
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
});
studentSchema.statics.findAllMajorStudents = function (major) {
return this.find({ major: major }).exec();
};
const Student = mongoose.model("Student", studentSchema);
Student.findAllMajorStudents("CS")
.then((data) => {
console.log(data),
})
.catch((e) => {
console.log(e);
});
執行 node app.js:(會找到所有 major: "CS" 的資料)
{
scholarship: { merit: 0, other: 0 },
_id: new ObjectId('65f911e5afc22b0ab78bf204'),
name: 'Spancer Kwan',
age: 37,
major: 'CS',
sholarship: { merit: 3000, other: 2000 },
lastModified: 2024-03-19T09:52:38.014Z
}
第三種方法:
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });
範例:
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
});
studentSchema.static("findAllMajorStudents", function (major) {
return this.find({ major: major }).exec();
});
const Student = mongoose.model("Student", studentSchema);
Student.findAllMajorStudents("CS")
.then((data) => {
console.log(data),
})
.catch((e) => {
console.log(e);
});
執行 node app.js:(會找到所有 major: "CS" 的資料)
{
scholarship: { merit: 0, other: 0 },
_id: new ObjectId('65f911e5afc22b0ab78bf204'),
name: 'Spancer Kwan',
age: 37,
major: 'CS',
sholarship: { merit: 3000, other: 2000 },
lastModified: 2024-03-19T09:52:38.014Z
}
Mongoose Middleware
Mongoose Middleware (也稱為 pre, post hooks) 是在異步函數執行期間傳遞控制權的函數。Middleware 是定義在 Schema 上的。
例如:我們可以定義 schema.pre(save’, callbackFn) 這個 Middleware。當任何與此 Schema有關的物件要被儲存之前,此 pre hook 內的 callbackFn 就會先被執行。
同理,若定義 schema.post(save’, callbackFn) 這個 Middleware,則任何與此 Schema 有關的物件被成功儲存之後,此 post hook 內的 callbackFn 就會被執行。
範例:
const fs = require("fs");
const studentSchema = new Schema({
name: {type:String, require: true, maxlength: 25},
age: {type:Number, min:[0, "年齡不能小於0"]},
major: {
type: String,
required: function () {
return this.scholarship.merit >= 3000;
// 如果 scholarship.merit >= 3000 則此資料為必填
enum: [
"Chemistry",
"Computer Science",
"Mathematics",
"Civil Engineering",
"undecide",
],
// 如果填寫的資料不是上述的其中一個,則會報錯!
},
},
scholarship: {
merit: {type: Number, default: 0},
other: {type: Number, default: 0},
// 預設值為 0
},
});
studentSchema.pre("save", () => {
fs.writeFile("record.txt", "A new data will be saved...", (e) => {
if (e) throw e;
});
});
const Student = mongoose.model("Student", studentSchema);
let newStudent = new Student({
name: "小明",
age: 30,
major: "Computer Science",
scholarship: {
merit: 5000,
other: 1000,
},
});
newStudent
.save()
.then((data) => {
console.log("資料已經儲存");
})
.catch((e) => {
console.log(e);
});
執行 node app.js:
會儲存一筆小明的資料,並新增一個 record.txt 檔案,裡面會記錄:A new data will be saved...