# [BE201] 後端中階:Express 與 Sequelize
###### tags:`Backend`
[TOC]
## Apache + PHP 組合 vs express
瀏覽器傳送 url "/a.php",apache 這個 server 會去尋找對應名稱("/a.php") php 檔案。所以 url 跟路由名稱是綁死的。
express 同樣傳送 url "/a",但後端不需要對應名稱的路由檔案("/a.php"),也就是不用 php,可以直接傳送 response,像這樣
```javascript=
app.get('/hello', (req, res) => {
console.log(`come from /hello`)
res.send('/hello + how are you')
})
```
### 路由的名稱需要注意先後順序
> You'll want to use a method that is an exact-path-matching, like app.all. The app.use is prefix-matching, so if you app.use('/', foo) (the / is optional), then it matches all patches prefixed with that (which is basically every URL).
>
> Methods like app.all('/', foo) on the other hand are exact path matching, so it will run for all methods on exactly the path /.
> https://github.com/expressjs/express/issues/3443
## todos
`app.render(view, [locals], callback)`
render() 可以額外傳一個物件進去,物件可以被用在對應的 ejs 裡面輸出,例如這樣
```javascript=
const express = require('express')
const app = express()
const todos = ['first todo', 'second todo', 'third todo']
app.set('view engine', 'ejs')
app.get('/todos', (req, res) => {
console.log(`come from /todos`)
res.render('test.ejs', {
multiple: todos // key value 相同則可以省略
})
})
```
```javascript=
// test.ejs
<h1>Todos</h1>
<ul>
<% for(let i = 0; i < multiple.length; i++) { %>
<li><%= multiple[i] %></li>
<% } %>
</ul>
```
## express 的 middleware
middlare 就像一條生產線上的每一個產品加工區,req-res 之間依序會經過不同 middleware 處理,處理結束之後要用`next()`。
> 如果我想把參數傳給一個物件,它不是一個函式,不能用 middleware 但又想用 next 來處理,可以這樣做(使用 closure? 的感覺) https://stackoverflow.com/questions/21271492/conditionally-use-a-middleware-depending-on-request-parameter-express
middleware 分成很多種,express 內建,第三方,自己寫的函式...。
* Application-level middleware: 處理 req-res 間的流程
* Router-level middleware:
Router-level middleware works in **the same way as application-level middleware**, except it is bound to an instance of express.Router().
### expresss document
> Express is a routing and middleware web framework.
> Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.
"middleware function",middleware 就是個函式,接收 req, res, next 這三個參數,沒有使用 next() 的話永遠不會進到下一個 middleware。
> To load the middleware function, call `app.use()`, specifying the middleware function.
```javascript=
var myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
app.use(myLogger) // 每個 req進來都會執行這個
```
## ORM
ORM是甚麼呢?ORM全名是Object-Relational Mapping,中文是物件關係對映。顧名思義,就是將關聯式資料庫(Relational Database Management System)的資料,映射到物件(Object)之中,[出處](https://tecky.io/zh_Hant/blog/SQL%E4%B8%89%E9%83%A8%E6%9B%B2:%E4%BD%A0%E4%B8%8D%E9%9C%80%E8%A6%81ORM/)
## express 的 ORM : Sequelize
Sequelize will keep the connection open by default, and use the same connection for all queries. If you need to close the connection, call sequelize.close() (which is asynchronous and returns a Promise).
Sequelize is independent from specific dialects. This means that you'll have to install the respective connector library to your project yourself.
### N + 1 problem
當要查詢一(a)對多(b)關係的資料時,不是使用 left join 來做查詢,而是先查詢 a 的資料,再用 a.id 之類的方式來查出關聯的 b 資料,例如有一個 car table 和 wheel table,首先會先下`SELECT * FROM Cars;`,再對取得的 car 做 forEach() `SELECT * FROM Cars;`,第一個 query 做一次,第二個 query 做了 n 次。
https://malagege.github.io/blog/2019/05/21/%E6%9C%89%E9%97%9C%E7%A8%8B%E5%BC%8F-N-1-%E5%95%8F%E9%A1%8C/
目前看到的解決方法是:先把 wheel 的資料都取出來再自己做關聯,或是使用 sequelize 的 Eager Loading/Lazy Loading,[文件](https://sequelize.org/master/manual/assocs.html#fetching-associations---eager-loading-vs-lazy-loading)
### Migrations
Sequelize 有提供用 CLI 的方式來建立 tables 的操作方式,稱為 Migration,實作方法分為兩部分,一是規劃 tables 設定,二是執行 tables 設定。
使用它的好處大概就是可以免除 sync()( 但還是會用到.then() ) 的操作,程式碼會更好管理之類。
> V5: ... you can use migrations to keep track of changes to the database. With migrations you can transfer your existing database into another state and vice versa:
#### 安裝、init CLI
`npm install --save sequelize-cli`
`npx sequelize-cli init`
完成後專案跟目錄底下會多出這 4 個資料夾,`config/config.json`, `models/`, `seeders/`, `migrations/`.
記得要先去`config.json`來設定資料庫的連線方式。
> Model: Sequelize 用語,model 用來對 table 做操作,大概相似於 CRUD 語法,寫網頁的時候才會用到它來操作資料庫
> migrations: 負責把 table 搬家的資料,例如搬家到資料庫上
#### 規劃 tables 設定
首先下指令來產生規劃 table (User) 用的檔案,`npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string`,產生兩個對應的檔案:
```javascript=
// /models\user.js
// /migrations/20210711112441-create-user.js
```
`/models\user.js`,用來對 table 設定行為(LEFT JOIN 之類),用來給 express require() 用的,跟設定資料庫無關...but 下指令後也會產生這個檔案,所以寫在這邊。
`/migrations/20210711112441-create-user.js`,用來新增 table & 設定 table 的內容,實際在建 table 時使用的是它。
下完指令、產生檔案之後,還不會對資料庫有任何實際操作,下一步就是要實際執行這些設定。
#### 執行 tables 設定
下指令`npx sequelize-cli db:migrate`,來執行 `xxxxxxxx-create-user.js` 這個檔案,在 資料庫裏面產生 `users` 這個 table,第一次用的話還會產生 `sequelizemeta` 這個 table,用來記錄那些設定檔(xxxxxx-xxx-xxx.js) 執行過。

奇怪的細節:sequelize 會預設把資料庫的 table 名稱變成複數,userss > usersses,user > users,不想這樣的話可以自己做設定
### Association 把 table 做關聯
#### 先去 models 資料夾做設定
sequelize-cli 似乎沒辦法下太複雜的指令,要把兩個 table 做關聯的話,要自己手動去設定。
去 /models/ 底下做操作,加上這兩行,`Comment.belongsTo(models.User);`、`User.hasMany(models.Comment);`(只下一個的話,就只能單向拿資料),詳情去看實作的程式碼。
記得要先 require,
```javascript=
const db = require('../models')
const Comment = db.Comment
const User = db.User
```
* 如果要拿關聯後的資料,要記得下 include,例如`Comment.findAll({ include: User})`
* 記得關聯後的資料結構滿奇怪的,可以自己印出來看看,總之可以實際拿出來用的部分是`dataValues`
#### 設定完成之後,就可以開始快樂寫網頁拉
這裡分成兩個檔案`/controllers/comments.js`、`/controllers/users.js`,分別管理 comment 和 user 部分的邏輯。
#### 實作中遇到這個問題
實作後遇到`TypeError: require(...) is not a function 這個錯誤訊息`,找到一些解法,([解法1](https://github.com/sequelize/sequelize/issues/12990)[解法2](https://www.gitmemory.com/issue/sequelize/sequelize/12990/772508469)),原因好像是 node 版本太高了(解法1+2說的)?
查過要如何安裝不同版本的 nodejs,windows 可以用的方法普遍是說要用 nvm-windows 來管理版本,去看它的文件之後有提到滿多使用前的注意事項的,重點是要把先前安裝的 node 和 npm 刪乾淨會比較好(?),覺得不安,感覺我一刪之後就會爆炸XD,
更: [解3,我出錯的原因](https://shareablecode.com/snippets/solution-for-typeerror-require-is-not-a-function-while-using-sequelize-with-expr-vWFm-f34v),總結就是 sequelize-cli 會自己產生 models/ 這個資料夾,裡面"預設只能"放 sequelize-cli 用的資料,我操作的時候還放了我之前寫的 code。刪掉非sequelize-cli產生的檔案之後就正常了。
* ejs 裡面如果有未宣告的變數,會報錯。那如果想處理錯誤訊息怎麼辦??
## 部署:Nginx + PM2
### PM2
```
// 常用指令
npm install pm2 -g // 讓 SERVER 在背景執行
pm2 ls //viwe status
pm2 start index.js //run
pm2 logs "id"
pm2 info "id"
pm2 stop "id" //stop
pm2 restart "0"
pm2 delete "0"
```
### Nginx
client > Proxy server > Server
一些 Proxy 用處:
* server 只知道 proxy 的 IP,不知道使用者的 IP(看 proxy 設定, 也可以讓 P roxy 顯示使用者 IP)
* 使用者可以偽造 IP 來源
* proxy server可以做 cache,常見 request 就不用再丟給 sever,減少 server 負擔
* 突破自身IP訪問限制
壞處是它可能是惡意 server
#### 反向的 Proxy: reverse proxy
client > Proxy server > Server a, b, c 之一...,使用者不知道自己的request 會送到哪個 server。
使用代理伺服器來接收使用者的 request,並傳給對應的伺服器,代理伺服器接收到伺服器回傳的 response 之後再傳給使用者
為什麼要這樣?
* 因為不想要在 url 欄位輸入 1.1.1.1:5001 之類的 port,看起來很醜而且會暴露使用哪個 port 或是伺服器的 IP。
* 使用 reverse proxy 的話就可以做到例如:使用80 port 做為總入口,根據要求(subdomain)再導到不同門(port, server),也就是一種負載均衡
* 提供對基於Web的攻擊行為(例如DoS/DDoS)的防護
> [這篇用房東的比喻滿好理解 proxy 概念](https://cloud.tencent.com/developer/article/1418457)
### Install ngnix on ubuntu
> [how to?](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-18-04)
ngniz.conf
影片,6:30
這邊我沒有跟著做。
## heroku
免費狀態是按小時來記的,沒人使用的話會自己休眠(不計時)
### 前置觀念:環境變數
process.env.varName // 在 nodejs 取得環境變數(varName)的方式
cli 端:
```javascript=
// 記得不要有空格
export varName=123 //儲存環境變數
varName=123 //賦值, 關掉 cli 之後就不見
echo $varName //印出
```
### 部署實作(沒有 DB)
heroku 使用 git bash 登入 heroku cli 來做部署,port 則是使用環境變數來指定,跟之前的寫法會有不同,以及heroku的執行方式是
已經申請好 heroku 帳號,就是跟著官方文件走。
[官方教學](https://devcenter.heroku.com/articles/deploying-nodejs)
開一個全新資料夾 > npm init > npm i express(以及 heroku cli) > touch app.js > (給 app.js 加上程式碼)
這裡要注意 port 不能寫死,要讓它接收環境變數,例如範例的第 3 行。
其他要做的有(官方有寫):
* Specify the version of node
* Specifying a start script
```javascript=
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log('3000 is listening now')
})
```
都做完之後就進行:Deploy your application to Heroku(官方有寫)
### 部署實作(有 DB)
DB 使用 heroku 提供的 ClearDB MySQL,這次使用的是 heroku 那邊的資料庫,所以要做 migration。
#### setup heroku
之前做過了。
[官方文件](https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up)
#### Deploy the app
之前做過的部分省略,就是照著官方文件跑,完成之後會說網頁有錯誤之類的訊息,輸入網頁錯誤提供的指令來查看 err logs。
* 記得 push 上去之前要做好 `.gitignore`檔案內容
* 除了影片提到的原因之外(要 migration、環境變數、script 設定),因為我上次跟著影片做出來的留言板使用的是 mariaDB,然後 heroku 那邊是用 mysql,npm 的套件設定出錯了
* 環境變數`const port = process.env.PORT || 5001`
* 這邊有點亂,總之想達成的目標是部署之後,可以和 ClearDB MySQL 連線,部署的程式碼已經有寫好 DB table 了,所以要在 heroku 新增對應的 table,所以做 migration。migration 也要做環境變數,`models/index.js` 的這邊↓有說道 migration 會去`config/config.json`這個檔案來跟 DB 連線,注意還是要上傳 config/,雖然我們要連的是 heroku 那邊的資料庫,但 migration 的相關設定之前已經做好了,這次的部署只是把已經可以在 localhost 執行的程式碼丟上去而已,並沒有在 heroku 上面做 migration 的前置工作。那麼要怎麼 mgration 呢? 靠下面提供的腳本。
```
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env]; // config json 負責跟 DB 連線
if (config.use_env_variable) { // 要執行這個 if
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
```
```
// `config/config.json`這個檔案新增這段
"production": {
......
"use_env_variable": "CLEARDB_DATABASE_URL" // 環境變數名稱看下方圖片
}
```
* sequelize-cli 好像被我手動移除掉了(當時以為它不會再被用到),migration 的時候也出錯
* .gitignore 不要連 migration資料夾也一起放進去了,
* 上傳的 js 是經過 babel+ugligy 的版本,報錯的時候超難 debug
* package json 做設定,db:migrate 這個指令來自於 [sequelize 文件](https://sequelize.org/master/manual/migrations.html#running-migrations)
```
"scripts": {
"db:migrate": "npx sequelize db:migrate", // migration
"test": "echo \"Error: no test specified\" && exit 1",
"start": "npm run db:migrate && node index.js"
},
"engines": {
"node": "14.x"
},
```
圖片(沒存到...):