[Node.js] MongoDB

前面學習了模板及中介軟體的使用,這裡要再學習數據庫,在其中儲存數據,然後從我們的應用程式獲得數據並將其注入到模板。

什麼是 MongoDB

數據庫可以分為兩種,一種為 SQL 關聯式資料庫,另一種則為 NoSQL 非關聯式資料庫。兩者的數據結構不一樣,SQL 使用 tables 的 rows、columns 儲存數據記錄,NoSQL 使用 collections 及 documents。

這裡我們要學習名為 MongoDB 的 NoSQL 數據庫,其會將資料分成好幾個 collections 有些像 SQL 的 table,每個 collection 將用於儲存特定類型的資料,例如有一個 user collection 儲存用戶的 documents、另一個 blog collection 則儲存 blog 的 documents。



而每個 document 則像 SQL 的 record 記錄,每個 document 代表單一個數據項目,例如一個 blog document 代表一個部落格文章的數據,並以 JSON 的格式儲存

而我們可以連接到 MongoDB 對這些 document 進行讀取、建立、更新、刪除,後面就讓我們學習如何建立數據庫以及將應用程式與數據庫連接起來。

設置 MongoDB Atlas

接下來為使用 MongoDB 做準備,可以選擇在本地電腦上安裝 MongoDB 然後使用、部署它,或者可以選擇有託管服務的雲端數據庫,這對於我們來說更容易的管理它,所以這裡我們採用雲端數據庫的方式。

MongoDB Atlas 是 MongoDB 公司的雲端資料庫服務,透過該服務來創建數據庫。

  • 首先進入 MongoDB Atlas 的網站,點擊 Start free


  • 在這裡註冊免費的帳戶


  • 註冊完成後會自動來到管理你的 MongoDB 的頁面,也許外觀會不太一樣,但功能會是一樣的


  • 點擊 Create an Organization


  • 輸入 Organization Name,並點擊下一步


  • 點擊 Create Organization


  • 來到以下畫面,點擊 New Project 新增專案


  • 輸入專案名並繼續


  • 點擊 Create Project


  • 跳轉到以下畫面,點擊 Build a Cluster 建立集群


  • 選擇免費方案,適合用於學習或開發小型應用程式


  • 接著選擇供應商及放置區域還有一些其它選項,可以使用預設即可


  • 其中一個選項為命名集群名稱,一旦建立便無法更改,預設為 Cluster0,這裡我改成 Test


確認沒問題後,按下 Create Cluster 並等待幾分鐘 Cluster 的建立。

  • 出現以下畫面,接下來要創建 database 以及 collection


  • 點擊 COLLECTIONS


  • 點擊 Add My Own Data


  • 輸入 database 名稱及 collection 名稱,點擊 Create


  • 來到以下畫面,目前還沒有建立 document


現在我們要做的是建立一個 user 用戶,並使用該 user 從我們的應用程式代碼來訪問數據庫

  • 點擊左側 Database Access 選項


  • 點擊 Add New Database User


  • 身分驗證方法選擇 Password


  • 設定 user 的帳密,另外用戶對數據庫的權限設定為授權讀取及寫入,按下 Add User


  • 現在已新增一個使用者了,這在後面的連接數據庫會派上用場


  • 回到 Clusters 欄位


  • 點擊 CONNECT


  • 設置哪些用戶和IP地址現在可以訪問您的群集,點擊 Add Your Current IP Address


  • 將目前的 IP 地址加入


  • 然後點擊 Choose a connection method,選擇 Connect your application 選項,與我們的應用程式連接


  • 點擊 Copy 複製該字串,一會兒要用到


  • 回到 app.js 將剛剛複製的字串存在常數,用於稍後連接到數據庫

    1
    2
    // connect to mongodb
    const dbURI = 'mongodb+srv://joe:<password>@test.r1uyr.mongodb.net/<dbname>?retryWrites=true&w=majority';

但注意以上字串中 <password> 要更改為使用者的密碼 test1234,以及 <dbname> 要改為數據庫名稱 test,如下

1
2
// connect to mongodb
const dbURI = 'mongodb+srv://joe:test1234@test.r1uyr.mongodb.net/test?retryWrites=true&w=majority';

如果有多個使用者可能會有 <username> 也需要更改

接下來我們可以使用 MongoDB 常規的 API 進行連結、查詢數據庫,但會有些冗長的代碼,因此接下來使用稱為 Mongoose 的東西來使其變得更容易使用些。

Mongoose, Schemas & Models

Mongoose 其實是一個 ODM library,ODM (Object Document Mapping) 是一種把數據庫操作抽象化的程式技巧,將數據庫的數據表示為 JavaScript 物件並與數據庫做綁定。

Mongoose 為我們提供更輕鬆的方式連接 MongoDB,創建具有數據庫查詢方法的模型,能夠創建、獲得、刪除、更新 document,當然這些用 MongoDB 提供的 API 也能夠做到。

在開始使用 Mongoose 之前先了解 Schemas 以及 Models

Schemas 模式
在 Mongoose 中使用數據庫的資源時,要先創建一個 schema 用來定義數據庫中的數據類型或 document 的結構,描述應該具有哪些類型屬性,像是

  • user 的 document 可能包含 name(string)、age(number) 屬性
  • blog 的 document 則包含 title(string)、snippet(string) 屬性

像這樣定義應該有什麼樣的屬性及是什麼類型的數據。

Models 模型
創建完 schema 後,接著根據該 schema 創建一個 model,而該 model 則允許我們與特定的數據庫做溝通,例如我們基於 blog schema 建立一個 blog model,而該 model 具有從數據庫的 blog collection 讀取或刪除數據等方法可以使用。

接下來開始使用 Mongoose,可以參考 Mongoose 的官方文件操作

  • 首先 Mongoose 是第三方套件,所以在終端輸入 npm install mongoose 安裝,接著在 app.js 中引入 mongoose 模組

    1
    const mongoose = require('mongoose');

  • 使用 mongoose.connect() 與數據庫做連接,參數會用到前面複製的 dbURI,第二個物件參數則是使用新的 URL 解析器,避免執行時跑出棄用警告

    1
    mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true });

  • mongoose.connect() 會回傳類似 promise 的訊息,所以我們可以將 then() 附加其後,由於 mongoose.connect() 為非同步函數,我們不希望在連接到數據庫前伺服器便開始監聽端口,所以將執行監聽的 app.listen() 移到 then() 當中,當成功連接數據庫時便會開始監聽,倘若失敗則顯示錯誤訊息在伺服器端。

1
2
3
4
5
6
7
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then((result) => {
app.listen(3000);
})
.catch((err) => {
console.log(err);
});

下一步要為我們的部落格數據建立 schema 及 model

  • 新增一個 models 資料夾用來存放 schemas 及 models,建立 blog.js 編碼 schema


  • 在 blog.js 編寫以下代碼

    1
    2
    const mongoose = require('mongoose');
    const Schema = mongoose.Schema; // 定義文件結構的介面

    Schema 是建構函數,我們要用它創造文件結構


  • 定義結構方式如下,使用 new Schema(),參數為一個物件,設定屬性名如 title, snippet, body,屬性值設定該屬性類型及是否為必須的屬性

    1
    2
    3
    4
    5
    6
    7
    8
    const mongoose = require('mongoose');
    const Schema = mongoose.Schema; // 定義文件結構的介面

    const blogSchema = new Schema({
    title: { type: String, required: true },
    snippet: { type: String, required: true },
    body: { type: String, required: true }
    });

  • new Schema 還有第二個可選的參數也是物件可以設定多個選項,根據官方文件有以下選項可選

這裡我們使用 timestamps

1
2
3
4
5
6
7
const blogSchema = new Schema({
title: { type: String, required: true },
snippet: { type: String, required: true },
body: { type: String, required: true }
}, {
timestamps: true
});

表示會為我們的 document 自動產生時間戳記屬性,根據官方文件描述會分別產生記創造時的時間戳記及更新時的時間戳記屬性,這點後面結果可以看到。

定義好了 schema 接著建立 model,schema 是定義文件結構的東西,而 model 則是包覆著 schema 的東西,並提供與該文件數據庫的 collection 進行通訊的接口。

  • 使用 mongoose.model() 建立模型並存在 Blog 常數,通常以首字大寫命名,注意mongoose.model() 第一個字串參數,將會以該字串的複數化字串在數據庫中尋找 collection,例如這裡會尋找 'blogs'而不是 'blog' collection,第二個則是前面定義的文件結構 schema

    1
    const Blog = mongoose.model('Blog', blogSchema);

  • 導出該 model 以便在其它地方也能夠使用

    1
    module.exports = Blog;

blog.js 全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
const mongoose = require('mongoose');
const Schema = mongoose.Schema; // 定義文件結構的介面

const blogSchema = new Schema({
title: { type: String, required: true },
snippet: { type: String, required: true },
body: { type: String, required: true }
}, {
timestamps: true
});

const Blog = mongoose.model('Blog', blogSchema);
module.exports = Blog;

現在 schema 及 model 都有了,接著開始與數據庫做溝通

  • 在 app.js 中引入剛剛的 Blog 模組

    1
    const Blog = require('./models/blog');

  • 建立處理'/add-blog' 請求的中介軟體,使用new Blog() 創建 blog document 的新物件,而參數則是包含我們要新增到數據庫的內容屬性,記得這些屬性要與定義的 schema 匹配,最後使用blog.save()儲存在數據庫

    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.get('/add-blog', (req, res) => {
    const blog = new Blog({
    title: 'new blog',
    snippet: 'nice to meet you',
    body: 'blah blah blah'
    });

    blog.save();
    });

    以上使用 Blog model 所以 mongoose 會自動的在數據庫搜尋 blogs collection,使用 save()blog 保存到 blogs collection 當中。


  • save() 為非同步函數,同時也會回傳 promise 物件,所以做以下處理,若執行成功將回傳的結果發送給瀏覽器顯示,失敗則在伺服器端顯示錯誤訊息

    1
    2
    3
    4
    5
    6
    7
    blog.save()
    .then((result) => {
    res.send(result);
    })
    .catch((err) => {
    console.log(err);
    });

app.js 全部的 code

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const express = require('express');
const morgan = require('morgan');
const mongoose = require('mongoose');
const Blog = require('./models/blog');
const app = express();

// connect to mongodb
const dbURI = 'mongodb+srv://joe:test1234@test.r1uyr.mongodb.net/test?retryWrites=true&w=majority';
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then((result) => {
app.listen(3000);
})
.catch((err) => {
console.log(err);
});

app.set('view engine', 'ejs');

// app.listen(3000); //移到 mongoose.connect()

app.use(express.static('public'));

app.use(morgan('tiny'));

// mongoose and mongo sandbox routes
app.get('/add-blog', (req, res) => {
const blog = new Blog({
title: 'new blog',
snippet: 'nice to meet you',
body: 'blah blah blah'
});

blog.save()
.then((result) => {
res.send(result);
})
.catch((err) => {
console.log(err);
});
});

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

  • 現在執行,連接 localhost:3000/add-blog 顯示如下
    (註:筆者有安裝 JSONView 的擴充功能所以才能有以下自動縮進換行的顯示,如果沒安裝會看到一坨的字串)

    看到第一項 _id 是自動生成的屬性,title,snippet,body 是透過 mongoose 設定的屬性,createdAtupdatedAt 也是透過 mongoose 自動產生的時間戳,至於 __v 也是自動產生的,我想應是更新版本號。


  • 開啟 MongoDB Atlas 的 cluster 可以看到也新增了一筆一樣的 document


  • 現在回到 app.js 將 blog 的 title 改成 'new blog 2'

    1
    2
    3
    4
    5
    6
    7
    8
    app.get('/add-blog', (req, res) => {
    const blog = new Blog({
    title: 'new blog 2',
    snippet: 'nice to meet you',
    body: 'blah blah blah'
    });

    ...

  • 儲存執行,再一次連上 localhost:3000/add-blog 回傳如下

    MongoDB Atlas 會看到有兩筆 documents

現在已經能夠將數據存入數據庫了。

  • 現在要獲取所有的 blog document,在 app.js 添加以下代碼

    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.get('/all-blogs', (res, req) => {
    Blog.find()
    .then((result) => {
    res.send(result);
    })
    .catch((err) => {
    console.log(err);
    });
    });

    使用到 Blog 模型的 find() 方法獲取 blog document,也是非同步函數,獲取後將結果發送回瀏覽器。


  • 執行,連接 localhost:3000/all-blogs

    得到一個陣列,裡面包含兩個物件


  • 也可以針對某一個 document 尋找,與 find() 方法差不多,只是使用 findById() 方法,需要資料裡的 _id 作為參數找到該特定的 document,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.get('/single-blog', (req, res) => {
    Blog.findById('5f460fb21c060b1a64a0981c')
    .then((result) => {
    res.send(result);
    })
    .catch((err) => {
    console.log(err);
    });
    });

  • 執行,連接 localhost:3000/single-blog 顯示特定的 document

如果將 findById() 的 id 修改為另一個便會得到另一個 document

輸出 document 在 views

我們已經能與數據庫連接並做數據上的交流,現在要修改 app.js 中的路徑,並讓 document 顯示在原先的網頁當中。

  • 首先將剛剛做的有關 blog 數據操作的中介軟體都刪除掉,再將 app.get('/',...) 修改為以下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 原來的 app.get('/', ...)
    // app.get('/', (req, res) => {
    // const blogs = [
    // {title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
    // {title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
    // {title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
    // ];

    // res.render('index', { title: 'Home', blogs});
    // });

    app.get('/', (req, res) => {
    res.redirect('/blogs');
    });

  • 添加以下 blog 中介軟體,使用 find() 獲取所有的文件,再將該結果渲染到 views 當中,另外在 find() 後面接上 sort() 將獲得的文件進行排序, createdAt: -1 表示根據 createdAt 屬性降序排序,也就是最新排到最舊

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // blog routes
    app.get('/blogs', (req, res) => {
    Blog.find().sort({createdAt: -1})
    .then((result) => {
    res.render('index', { title: 'All Blogs', blogs: result })
    })
    .catch((err) => {
    console.log(err);
    });
    });

    其實也不一定要這麼做,直接寫在 '/' 的中介軟體也可以,但這裡我們想要另外管理負責 blog 的 route。


app.js 全部的 code

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const express = require('express');
const morgan = require('morgan');
const mongoose = require('mongoose');
const Blog = require('./models/blog');
const app = express();

// connect to mongodb
const dbURI = 'mongodb+srv://joe:test1234@test.r1uyr.mongodb.net/test?retryWrites=true&w=majority';
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then((result) => {
app.listen(3000);
})
.catch((err) => {
console.log(err);
});

app.set('view engine', 'ejs');

app.use(express.static('public'));

app.use(morgan('tiny'));

app.get('/', (req, res) => {
res.redirect('/blogs');
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

// blog routes
app.get('/blogs', (req, res) => {
Blog.find().sort({createdAt: -1})
.then((result) => {
res.render('index', { title: 'All Blogs', blogs: result })
})
.catch((err) => {
console.log(err);
});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

  • 執行,連接 localhost:3000

    將每一筆的 document 數據顯示在頁面上了。

參考資料
The Net Ninja | Node.js Crash Course Tutorial #9 - MongoDB