[NodeJS Become A Full Stack Developer] — NodeJS ORM Sequelize Cli 串接資料庫

MySQL

Introduction & 前言

好久不見,在開始後端之路之後進度雖然緩慢,但發現一路上的確挺有趣的,經歷過了 [NodeJS Become A Full Stack Developer] — 從0開始 NodeJS 小試身手 透過 NodeJS 重頭開始認識後端,一直到 [NodeJS Become A Full Stack Developer] — 菜雞必經之路 👉 實作一個 Todo List 我們透過了三篇文章介紹如何轉職為全端勇者。

在今天這篇文章將來探討如何串接 **MySQL(資料庫)**,而非上次透過 Firebase 快速串接,這次將會是有趣的一篇文章(才怪)!


Summary & 摘要

Connect

在前一章筆者實作了陽春版本的 TODO List,提到後端就不得不提資料庫,而在 TODO List 之中我們使用的是 Google Firebase 的資料庫,雖然都是資料庫但是和我們要使用的 MySQL 是不相同的。

本篇文章會談到幾個點:

  1. 資料庫差異

  2. 何謂 ORM

  3. 如何串接資料庫

  4. 快速上手 Sequelize

  5. 完成人生第一個後端資料


資料庫差異

SQL(Structured Query Language 結構化查詢語言) 是一種專門用來管理與查詢關聯式資料庫(Relational database)的程式語言 在某些網站都能看見對關於 SQL 的介紹文,這邊講太多太細的東西大家肯定也記不住,既然我們是幼幼班上手,就從好記得來。

關聯式資料庫(Relational database)

首先來說說 關聯式資料庫 的幾個特點:

  1. 資料是一個或多個資料表方式存放

  2. 資料之間有關聯性

  3. 以 SQL 語言操作

關連式資料庫

關連式資料庫會把每一個表格集合起來,而每一個表格裡面都是一筆一筆的資料,不同資料表可能會互相關聯著,比如 **會員小明(Table -> Users)訂購了(Table -> Orders)一支Iphone 12 pro max(Table -> Goods)**,這之間的表格都互有關連,在前端發送出請求一直到後端時,後端可以透過 SQL(註1) 語法去將資料庫裡的資料進行收集、排序,亦或是新增刪除。

註1: SQL語法拿來在操作關聯式資料庫時使用,例如 SELECT * FROM Users Where age=1(將表格 Users 裡面,符合條件 age = 1 的資料取出)

非關聯式資料庫(NoSQL)

照字面上的意思應該就能懂八十七層;因為現今網路應用程式流量越來越大幅成長,對於關聯式資料庫在存取大量資料時,會比較慢,你能想像在使用 YoutubeFacebook 時,跑個資料就跑好幾分鐘的感覺嗎?我想應該連幾秒都不太想等待吧!

而在需要快速取得資料的情況下人們開始做取捨,不需要即時同步且精準無誤的取得某些特定資料,所以 NoSQL 就這樣熱門了起來。

NoSQL 的全名為 Not Only SQL,在 NoSQL 上並不支持 SQL 語法,且更關注於資料的變動,舉例來說 Youtube 的瀏覽數及訂閱數量在某些百萬級 Youtube 會是數萬甚至數十萬筆,這時候點開我們並不需要先去確認誰先訂閱,或是誰比較晚訂閱,只需要注重最後的結果,最後統計是多少的訂閱數量,這就是適合使用 NoSQL 達到的,相反的如果需要在乎於邏輯上,比如說電商網站,先刷卡、確定扣款後、商品才能寄出,這時候適合什麼應該很清楚了吧!

最後 NoSQL 有一個最特別的點,所有資料都會被用 JSON 方式存放起來,每一筆資料都是 Key:Value(name: ‘王小明’) 的方式,而這些資料則會被存放在 Documents,最後一堆一堆的 Documents 則會被放在 Collections 裡。


何謂 ORM

在開始講如何串接資料庫前這邊必須在提到 ORM,簡單來說在上方我們有提到 SQL 語法,而你會想這樣我不就要再多學一個語言嗎!

別擔心已經有人幫你想好了,俗話說的好,學會一個語言再去學另一個語言會很快,那如果我們學好一個語言再用這個語言的邏輯去學另一個語言,是不是又再縮短一半的時間了呢?

舉個例子來說,我們已經會 Javascript 了,我們在使用 Javascript 去學習 NodeJS,而我們認識了 NodeJS 了,再透過 Javascript 去學習 SQL,有沒有一種健達出奇蛋的感覺呢?買了一顆健達出奇蛋(Javascript),滿足你三種願望(Vue、NodeJS、SQL)!

我知道比喻很爛別揍我

ORM 就是有開發者透過程式語言的物件去包裝 SQL,讓你可以透過你熟悉的程式語言(Javascript、PHP)去簡單呼叫層層包裝過的方法;只是有一點要特別注意,上方提到的 關聯式資料庫非關聯式資料庫,前者透過 ORM 技術,後者透過 ODM

例如: UsersDB.findAll() -> SELECT * FROM USERS;


如何串接資料庫

終於到我們的重點了,這邊會先示範如何串接資料庫不透過 ORM,首先輸入下列內容到後端專案裡。

1
$ npm i mysql2 -S

接著在後端入口(app.js),引用資料庫並且連接資料庫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var mysql      = require('mysql2');
var connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : '123456',
database : 'users'
});

connection.connect();

connection.query('SELECT * FROM users', function (error, results, fields) {
if (error) throw error;
console.log('The Users is: ', results);
});

完工!雖然這樣就能簡單的連上了,但東西都塞在入口檔案,和我們想要的 MVC 環境架構還有 87% 遠,而你也不會想要每次建立後端專案就手幹一次吧,所以接著們要提到本日重頭戲了。


快速上手 Sequelize

起手式

Sequelize 就是剛剛上方提到的 ORM,而他也具備了幫我們初始化環境的功能,我們按照步驟來吧。

1
2
3
$ npm i sequelize sequelize-cli mysql2 -S // 安裝相關套件

$ npm i sequelize-cli -g // 全域安裝 ORM Cli

雖然 sequelize 作為類似套件方式使用,安裝在專案裡面,但跟 GulpVue-cli 一樣,我們可以透過 cli 來使用某些特定的功能;緊接著輸入下面的內容:

1
$ sequelize init

這時候你會看見專案裡面多個幾個資料夾,分別為

  • config

  • models

  • migrations

  • seeders

這時候這個套件已經幫我們把 MVC 分別分類出來了。

環境介紹

筆者個人認為這個框架對新手算是友善的,因為你可以透過這個框架認識 MVC 架構如何分,也可以再透過這個框架去操作資料庫。

讓我們先看到 config 資料夾裡面的 config.json

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
{
"development": {
"username": "root",
"password": null,
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
}
}

這邊便是我們要讓 Sequelize 知道在什麼情況下該與什麼資料庫連接,由最上開始分別為 開發、測試正式,而我們目前只會先用到開發,所以我們先把最上面的 development 做點更動。

1
2
3
4
5
6
7
8
9
10
11
{
"development": {
"username": "root",
"password": null,
"database": "test",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorsAliases": false
},
// ...下略
}

然後我們再看到 model 資料夾下的 index.js

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
'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}

fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});

Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

這邊大致上可以看過,如果有興趣可以在深入研究,基本上這邊就是在利用同目錄底下的其他 .js 檔案的 model.name 當作索引,接著放到 db 物件裡,執行時 modeldefine 將資料表與 js 對應上。

在簡單的說就是之後我們建立在 model 資料架內的任何 .js 檔案,都會建立好關聯,在需要呼叫 DB 的時候只需要使用 const Users = require('./model').Users;const Goods = require('./model').Goods; 這樣就能呼叫了。

小整理

介紹完前面兩個資料夾後,我們先整理一下:

  • config - 設定什麼環境應該連接什麼資料庫的地方

  • models - 存放各個 DB 表格的地方,需要使用到可以直接透過 const Users = require('./model').Users; 方式呼叫

migrations

講到這個可能剛接觸後端是第一次看見,但如果有與後端合作過,可能會聽見後端請你跑類似 php artisan migration 的東西,你就會發現資料庫內的表格都神奇地被建立好了。

這邊我們需要先使用下面語法去產生我們的 DB 然後把要新增上資料庫的表格,先一併存放在 model 資料夾裡。

1
$ sequelize model:generate --name users --attributes mobile:string,email:string,birthday:time,sex:tinyint

上方 –name 後方為表的名稱,而最後面的就是要建立的 keyvalue 的種類。

這樣就會在 model 資料夾內建立我們的第一個表,仔細看看 models/users.js

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
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class users extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
};
users.init({
mobile: DataTypes.STRING,
email: DataTypes.STRING,
birthday: DataTypes.TIME,
sex: DataTypes.TINYINT
}, {
sequelize,
modelName: 'users',
});
return users;
};

然後 migration 資料夾也新增了一個檔案 2020XXXXXXXXXX-default-users

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
'use strict';

const UUIDV4 = require('uuid/v4');

module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: UUIDV4
},
mobile: {
type: Sequelize.STRING,
comment: '手機號碼'
},
email: {
type: Sequelize.STRING
},
birthday: {
type: Sequelize.DATE,
comment: '生日'
},
sex: {
type: Sequelize.ENUM,
values: [
'boy',
'girl'
],
comment: '性別為男=boy, 女=girl'
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users');
}
};

先解釋為什麼會引入 uuid/v4,一般的主鍵都是 +1+1 往上疊加上去,而這邊我想要給使用者一串亂數,例如 22a59k20-dff5-9285-1023-0u3a99ca6773,好處是不容易被破解,但因為想塞入這種亂碼就必須藉助套件去產生亂碼,所以想使用這個記得要另外安裝 **uuid**,之後 users 的主鍵就能獲得獨一無二的亂碼。

1
$ npm i uuid -S

上方的 type 即為種類,像是單純 字串Radio日期,其中裡面還能設定 備註 等等,可以上 Sequelize 的官網查看更詳細的種類。

介紹完之後就輸入 sequelize db:migrate 然後進入資料庫看看是不是有新增成功。

新增成功

我們會發現多了一個 SequelizeMetausers,前者就是用來紀錄你跑過多少個 migrate 的紀錄,後者為你剛剛上方設定的 Table 內容。

如果新增錯誤,想要重新來過,可以在輸入 sequelize db:migrate:undo 就會跑回去上一個步驟了,執行的內容為 migrations/2020XXXXXXXXXX-create-users.js 內的 function down()

seeders

說完建立 Table 當然就是要塞入預設假資料啦,不然每次開發你都要先手動填寫資料或是等到前端自己帶入資料才發現出問題就麻煩了。

seeders 的檔案也可以依靠指令產出來:

1
$ sequelize seed:generate --name default-users

執行結束後會發現 seeders 的資料夾下多了一個檔案,就是剛剛上方指令命名的 2020XXXXXXXXXX-default-users.js,接著裡面就必須自己輸入了,但套件很貼心的還是有範例在上方:

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
'use strict';

const { v4: uuidv4 } = require('uuid');

module.exports = {
up: async (queryInterface, Sequelize) => {
/**
* Add seed commands here.
*
* Example:
* await queryInterface.bulkInsert('People', [{
* name: 'John Doe',
* isBetaMember: false
* }], {});
*/
await queryInterface.bulkInsert('users', [{
id: uuidv4(),
mobile: '0900000000',
email: 'test@admin.com',
birthday: new Date('1995-08-08 08:08:08'),
sex: 'boy',
createdAt: new Date(),
updatedAt: new Date()
},
{
id: uuidv4(),
mobile: '0911111111',
email: 'test2@admin.com',
birthday: new Date('1904-05-20 12:49:00'),
sex: 'girl',
createdAt: new Date(),
updatedAt: new Date()
}], {});
},

down: async (queryInterface, Sequelize) => {
/**
* Add commands to revert seed here.
*
* Example:
* await queryInterface.bulkDelete('People', null, {});
*/
await queryInterface.bulkDelete('users', null, {});
}
};

migrate 一樣,我們必須透過指令來啟動它,輸入 sequelize db:seed:all 或是 sequelize db:seed:users 來指令要跑的種子即可塞入假資料,如果不想要預設資料了想清掉,就跑 sequelize db:seed:undo:all 就可以清掉所有的預設資料,或是指定目標清除預設資料 sequelize db:seed:undo:users

再度小整理

  • config - 設定什麼環境應該連接什麼資料庫的地方

  • models - 存放各個 DB 表格的地方,需要使用到可以直接透過 const Users = require('./model').Users; 方式呼叫

  • migrations - 存放要初始化 Table 的資料夾

  • seeders - 存放對應 Table 的預設資料


完成人生第一個後端資料

都完成後這時候可以到後端 routers 裡面,使用上方說的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
const UsersDB = require('../models').users;

router.get('/', (req, res) => {
(async () => {
let users = await UsersDB.findAll();

res.json({
status: 1,
msg: 'success',
users
})
})();
});

執行結果為下,大功告成!

1
Executing (default): SELECT `id`, `mobile`, `email`, `birthday`, `sex`, `createdAt`, `updatedAt` FROM `users` AS `users`;

Conclusion & 結論

這次是筆者第一次從 0 開始連接資料庫,然後寫上 migrateseeders,不得不說真的是滿滿成就感,不過需要學習的地方還是好多,雖然上面提到使用 ORM 等於是用自己熟悉的語言再去學習另一個語言,且被包裝過比起原生的 SQL 語法方便許多,但其實不論如何,碰到新的語言都還是要重新適應,畢竟這都是人家包裝過的東西,你就必須跟著作者的思路走,跟著作者的方法走。

最近對於程式的怠惰感及厭倦感又有點上升了,但絕非討厭程式,我想就跟運動一樣吧,剛開始滿懷熱血,但是持續了一陣子之後就會開始疲勞,這時候無非就是要自己尋找出自己的興趣,讓這件事情不再是單調枯燥乏味,希望在幾個月之後回頭看自己,能再有所成長。

剩下幾個月 2020 就要結束了,雖然今年很鳥,很多遺憾的實情發生了,但既然還活在當下就要好好把握,趁這段時間好好衝刺充實自己,期許正在看文章的人或是幾個月後的自己,都還是能對程式滿懷動力,並且讓自己越來越好。


參考網站