iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 15
1

回憶

前二天我們花了很多時間在講 Prmoie 和 async/await,對於到處都是非同步的 Node.js 來說是很重要的,務必多多練習。

目標

今天要來準備存放資料的地方 MongoDB 和如何存取裡面的資料。

  1. 安裝 docker
  2. 安裝並執行 MongoDB
  3. 使用 MongoDB driver for Node.js
  4. 使用 mongoose

Docker 架設 MongoDB

因為我希望二周目可以集中心力在講前後端,所以 Docker 的詳細使用會放到三周目,但我還是會保持最底限度的了解。

Docker 它可以安裝在不同的作業系統,它關鍵的技術是 容器(container)。當容器初次建立時會載入某個映象檔(image),映象檔打包著相關程式碼、函式庫、環境配置檔,然後容器就會執行在一個沙箱的執行環境。這技術跟模擬整個作業系統的虛擬機器不同,容器是更小的執行環境。

https://ithelp.ithome.com.tw/upload/images/20181015/20110371oq2vgIzKZz.jpg
上圖截取自 Docker Reference Architecture: Designing Scalable, Portable Docker Container Networks

因為容器是執行在 Docker Engine 上,且我們在三周目也會自己做映象檔並寫執行組態檔,所以我們要安裝 Docer Desktop,它的功能比較多。

用 docker 架設 mongodb

docker 環境

  1. 依照自己作業系統安裝
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371YFKrm9UbDo.png
  2. 灌完後執行 docker,就會有個常駐的小圖標https://ithelp.ithome.com.tw/upload/images/20181015/20110371TYVLlJ5paS.png,之後我們就可以用 docker 指令了

mongodb 容器執行

  1. 複製 hello-express專案(或繼續使用)改名成 hello-mongo。(若沒有 hello-express 的人可以看 Day 8 - 一周目- 開始玩轉後端(一))

  2. 下載 mongodb 的映象檔 (mongodb 官方印像檔 也有其它版本可以下載)

    docker pull mongo:4.1
    

    https://ithelp.ithome.com.tw/upload/images/20181015/20110371bHDdwD4cl4.png

    docker images 可以列出有下載的映象檔,看看有沒有下載成功

  3. hello-mongo根目錄建立一個 data 資料夾 mkdir data。用來放 mongodb 的資料,可以讓 mongo 容器刪除時可以留下資料,下次建立容器可以繼續使用

  4. terminal 移到專安根目錄後,建立容器且執行 mongo 容器

    docker run --name mongo4 -v $(pwd)/data:/data/db -d -p 27017:27017 --rm mongo:4.1
    

    docker ps 確認容器有執行
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371kAJ9Ce5kq6.png

    我們用映象檔 mongo:4.1 建立了名為 mongo4 的容器,掛載 hello-mongo 根目錄下的 data 資料夾到容器內。容器在背景執行,且對外的 port 號是 27017。當容器停止後自行移除。

  5. 確認 mongodb 資料庫有運行

    docker exec mongo4 mongo --eval "print(version())"
    

    https://ithelp.ithome.com.tw/upload/images/20181015/20110371vaFSznqwbn.png

  6. 若需要停止容器 (關掉 mongodb 資料庫),請輸入

    docker stop mongo4
    

    就會停止容器並刪除

到目前為止,我們完成了用 docker 架設 mongodb 資料庫

若要部署 docker mongo,應該還要加上安全性的設定(像是帳密),且用長駐的 docker container 執行(像是用 docker-compose 組態設定)

在 Node.js 中使用 MongoDB

架設完 mongodb 資料庫後,我們就可以用 client 端連入資料庫使用了。

mongodb官方提供很多程式語言的 driver (就是套件)連入資料庫,見
MongoDB Drivers and ODM
。我們的 Node.js 是 MongoDB Node.JS Driver

mongo 指令連入資料庫

另外,我強力建議開發時,安裝 shell(cli) 版本的 client,可以方便我們直接下 mongo 指令連線後做練習和測試,但不同的作業系統有自己的安裝方法,請見
Install MongoDB
。只是上面的安裝方法不僅安裝 shell 版還安裝伺服器版,也有只安裝 shell client 的方法,但請自己研究一下。

針對有潔癖的人、不想在本機端安裝的人,我提供一個方法使用 mongo 指令,就是:

進入容器內

因為 mongo 容器中除了資料庫的執行檔還有包含 shell client,所以進入容器就可以使用 mongo 指令,也不用擔心本機端安裝的 shell clinet 版本號不合資料庫版本號。

  1. 輸入 docker exec -it mongo4 bash,就可以進入容器內
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371rJMSND1gVW.png
  2. 就能用 mongo 指令,連入資料庫
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371WAJfH0zDIO.png
  3. 不用的時候直接關 terniaml 或 ctrl + c 和 exit 就可以離開容器
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371nHLY30zb9z.png

mongo 指令直接下時是連到本機端 localhost 的 27017 port 的預設值

Node.js Driver 連入

Node.js Driver 提供的非同步的函數,幾乎同時支援 callback 和 回傳 Promise,之後的例子我們儘量使用 Promise

  1. 安裝 Node.js Driver

    npm install mongodb --save
    
  2. 修改 ./router/index.js,使用 mongodb 套件,加入一個 GET /api/mongo api 來看 mongodb 是否連線成功

    注意: 下面的程式,每次打一次 API 都會建立一條連線,很浪費資源,晚一點我們會修改下面的程式碼

    const MongoClient = require('mongodb').MongoClient;
    
    router.get('/api/mongo', function (req, res, next) {
      // mongodb 位置
      const url = 'mongodb://localhost:27017';
    
      // 資料庫名
      const dbName = 'myproject';
    
      // 連立一個 MongoClient
      const client = new MongoClient(url, {useNewUrlParser: true});
    
      // client 開始連線
      client.connect()
        .then(() => {
          res.json({
            isConnected: true,
          });
        })
        .catch(error => {
          console.error(error);
          res.json({
            isConnected: false,
          });
        });
    });
    
  3. 用 Postman 打看看GET /api/mongo,若有連上 mongodb

    {
        "isConnected": true
    }
    

    若把 mongodb 關掉 (docker stop mongo4),會得到

    {
        "isConnected": false
    }
    

重用(reuse)連線

const client = new MongoClient(url);
client.connect();

會建立一條連線,一但接上,沒有手動 client.close() 或發生斷線,此連線不會釋放。因此,我們要留下已連線的 client,放在未來可以拿的到的地方

const MongoClient = require('mongodb').MongoClient;

// index.js 執行就建立連線
const url = 'mongodb://localhost:27017';
const dbName = 'myproject';
const client = new MongoClient(url, {useNewUrlParser: true});
client.connect()
  .then((connectedClient) => {
    console.log('mongodb is connected');
  })
  .catch(error => {
    console.error(error);
  });

router.get('/api/mongo', function (req, res, next) {
  res.json({
    isConnected: client.isConnected(),
  });
});

Collection 的 CRUD (Create, Read, Update, Delete)

上一節我們談了建立連線,現在要來操作資料,先來了解 MongoDb 的名詞對比 MySQL

type data collection element
MySQL(SQL) table record
MongoDb(NoSQL) collection document

MongoDB 的 document 有以下特性:

  • 物件資料:一個 collection 可放入很多 document,且 document的資料可以是 JSON 或 BSON (Binary JSON)。
  • 物件格式:collection 不用預先定義 sechma,也就是 document 想塞什麼物件都可以。

插入資料

修改 POST /api/echo,利用 Immediately-Invoked Function Expressions (IIFE) 立刻執行 async function(),寫入完成後才回應 request

router.post('/api/echo', function (req, res, next) {
  const body = req.body;

  // 寫入 MongoDb
  const worker = (async function (data) {
    const db = client.db(dbName);
    const collection = db.collection('echo');
    const result = await collection.insertOne(data);
    console.log(result);
    return result;
  })(body);

  // 回應
  worker.then(() => {
    res.json(body);
  })
    .catch(next); // 發生 error 的話,next() 交給之後的 middleware 處理,express 有預設的處理方法
});

確認資料有寫入

  1. shell 連入 mongodb
    mongo 127.0.0.1:27017/myproject
    
  2. 查看 echo 內的 document
    db.echo.find()
    
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371TTcSfh7K7w.png

怎麼查文件

可能的操作很多,自己查文件比較有效率,所以說說我怎看文件的,提供剛學習的人一些方向

一進入MongoDB Node.JS Driver,會看到 Reference 和 API
https://ithelp.ithome.com.tw/upload/images/20181015/201103718OaB6jiPfl.png

  1. Reference:你可能要學習使用,就選這個
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371AQsbYGC07R.png

    • Quick Start/Getting started:大部分的文件都會有這個,給想要立刻使用、體驗,不需要太多的預備知識。沒概念的人或新手就選這個玩一玩。
    • Tutorials: 學習一些必要知識的教學
      https://ithelp.ithome.com.tw/upload/images/20181015/20110371zbBuxuzY8w.png
  2. API:你已有使用概念,但想要查函數的定義/簽章或看看還有提供什麼函數?
    https://ithelp.ithome.com.tw/upload/images/20181015/20110371Obr0b4lhuX.png

    還可以查函數名,輸入 insert,就可以查到 Collection類別的 inserOne 函數定義
    https://ithelp.ithome.com.tw/upload/images/20181015/201103711kAi5Qkx2X.png

    例如:我要插入資料時

    1. 怎麼拿到 collection? db.collection('echo') 回傳 collection 物件,不是 promise
    2. 怎麼拿到 db:client.db(dbName) 回傳 db 物件,不是 promise
    3. 就可以插入資料
      const db = client.db(dbName);
      const collection = db.collection('echo');
      const result = await collection.insertOne(data);
      

    我寫文章的當下也是在查文件、看用法、看函數的簽章,不是憑空寫的

查文件的技巧是要練習的,也會越來越快。若沒概念就找 Quick Start/Getting started/Tutorials,或用 Google 找關鍵字看別人的用法,最後在學習查 API 文件或看官方教學文件。

另外,有個叫 Dash(for mac, iOS)的軟本可以查文件用(和程式碼自動輸入),也是可以用用看。我自己是用不習慣,所以我還是喜歡去官網看文件。

接下來,我們最後介紹 object modeling 套件mongoose 做為結尾。

使用 object modeling 套件:mongoose

有沒有發現 MongoDB Node.JS Driver 的 github 網址是 https://github.com/mongodb/node-mongodb-nativenode-mongodb-native 是官方提供操作 mongodb 用的底層套件,它提供的函數很底層,像我們之前的例子:

const db = client.db(dbName);
const collection = db.collection('echo');
const result = await collection.insertOne(data);

要一個個串起來使用。

當後端肥大起來似乎沒這麼方便,為了提供更高層級的思維,就有人寫出 object modeling / ORM(Object-Relational Mapping) / ODM(Object Document Mapper) 這類的套件,甚至不同資料庫、不同的程式語言大多有。

為了把資料(document/record)當做是物件,當操作物件時,套件會自己使用底層套件與資料互動。

你可以想想以自己寫 SQL 的經驗,ORM套件使你從串SQL文字解脫,變成物件的操作。但也不是這麼美好,因為你又要多學一個套件,不同的 ORM 也可能有不同的用法。

要不要用見人見智(見:Should I Or Should I Not Use ORM ?),我是覺得要用,寫起來比較快,但也要學會用底層套件,因為當你預到效能問題或ORM 套件有bug,才有解套的可能。我的學習策略是先用底層套件 mongodb,等用一陣子才引入 mongoose,你會了解用/不用 mongoose 後的酸甜苦辣。

Quick start

這例子來自mongoose 官方首頁, 直接表明了核心想法

// 建立連線
const mongoose = require('mongoose');
const dbName = 'myproject';
mongoose.connect(`mongodb://localhost:27017/${dbName}`); // resolve with Connection value

// 定義 "Cat" 的模型(也可以說是Schema),預設的 collection 的名稱會用復數、小寫,所以是 "cats"
const Cat = mongoose.model('Cat', { name: String });

// 建立物件
const kitty = new Cat({ name: 'Zildjian' });

// 存入資料庫
kitty.save().then(() => console.log('meow'));

有幾點要知道一下:

  1. mongoose 有一個預設的 connection,mongoose.connect() reolve value 是 Connectino 物件。若需要建立更多的連線可以用 mongoose.createConnection()(見:Multiple connections)
  2. CRUD 可能有型如:xxx()xxxMany()xxxOne()
  3. find() 是回傳的 Query,不是資料本身
  4. Query 和 Aggregate 使用時可能會用鍊式語法,最後才用 exec() 實際送出查詢

總結

今天我們利用 Docker 架設 MongoDB,還用 Node.js Driver 連入後插入資料。最後介紹 object modeling 套件 mongoose,方便我們對 MongoDB 操作。


上一篇
Day 14 - 二周目 - 從Promise 昇華到 async/await
下一篇
Day 16 - 二周目 - 強化後端專案結構
系列文
用js成為老闆心中的全端工程師31

尚未有邦友留言

立即登入留言