### GDSC NYUST x 資訊創客社
<br>
### JS後端與資料庫讀書會
### PostgreSQL with Express
<br>
#### 2023/11/29 ( Wed ) 19:00 - 21:00
#### 講師:蘇祐民 *YoMin Su*
#### 本次課程影片:(⚒️製作中)
<img src="" height="200px">
---
## 課程簡介
----
### 今天的目標
- PostgreSQL 入門
- 透過pg(資料庫連接器)與資料庫互動
- TypeORM 入門
- 透過ORM與資料庫互動
- 在Express中透過PostgreSQL存取資料
---
## PostgreSQL 入門
----
### 照慣例:簡介時間
PostgreSQL資料庫最初是柏克萊加州大學校內的Ingres研究計畫,該計畫的目標正是設計一套資料庫軟體,發展過程中,隨著不同的學生提出的各種需求與建議,軟體本身開始有了大量的使用者,也是在1985年左右,才確立了專案的名稱。
<div style="background-color: white">
<image src="https://hackmd.io/_uploads/B1FPUfEHa.png" width="300" />
</div>
----
### PostgreSQL 的商業化
而在商業化的方向上,歸功於PG選擇的BSD授權條款,讓這套資料庫在商業使用不會出現授權問題,在商轉這條路上,先後有多間公司嘗試過這條路,而對社群的協助則是,公司會安排專職的工程師一起對Postgres進行開發,讓專案持續的維護下去。
<div style="background-color: white">
<image src="https://hackmd.io/_uploads/HkSWFfVS6.png" width="300" /> <image src="https://hackmd.io/_uploads/H10qKfEH6.png" width="300" />
</div>
----
### 如何安裝PostgreSQL來玩?
兩種方法,擇一即可,可以根據需求選擇:
1. 裸裝,也就是直接安裝對應系統的套件
- 適合生產環境
2. 容器,透過PG官方提供的Docker映像
- 適合開發環節
----
### 這裡以Docker做舉例
以下是docker-compose範例
```yaml=
version: '3'
services:
db:
image: postgres:latest
environment:
- "POSTGRES_DB=<db_name>"
- "POSTGRES_USER=<username>"
- "POSTGRES_PASSWORD=<passwd>"
ports:
- "127.0.0.1:5432:5432"
```
----
### 執行一下

----
### 確認一下是否運作了

----
### 如何透過CLI工具連進資料庫?
Postgres有提供一系列工具用來進行管理,最常使用的,是`psql`,以下提供範例:
```bash=
docker compose exec -it db psql
```
----
### 你出錯了嗎~

出錯的原因很簡單,psql會以容器內的『預設使用者』嘗試進行連線,但發現找不到,於是乎就出現錯誤了
----
### 來補上缺少的參數
```bash=
docker compose exec -it db psql -U user -W test
```
小小解說:
- -U:指定使用者
- -W:輸入密碼
- -h:指定伺服器IP或Hostname
- -p:指定伺服器的Port
- test:這是資料庫名稱,要跟你設定的compose內的名字相同喔
----
### 應該連接上去了

----
### 取得基本資訊

其他可用的資訊,可以參考[官方文件說明](https://www.postgresql.org/docs/16/functions-info.html)
----
### 還記得前面設定的資料庫名稱嗎?

若你還沒注意到,可以看一下『=#』前面的字串,正常來說,就會是你設定的資料庫名稱!
----
### 如何查看資料庫內的資料表?
在MySQL或MariaDB中,通常是show tables;
但在psql中,有更簡單的方法,靠『\d』就好!

正常應該會像圖片的資訊,顯示沒有Relation
----
### 來新增資料表試試
如果你對標準SQL語法有概念,就太好了,大部分的用法都是通用的喔!
```sql=
create table student(
id varchar(9) primary key,
name varchar(20),
age int
);
```
----
### 看圖說故事

----
### 如果我想看表的定義可以怎麼做?
你可能會很訝異,但一樣可以用『\d』,加上表名就好!

----
### 有關表的定義
剛剛的那張圖,因為沒有複雜的關係,所以沒有太多資訊,但基本上會顯示的內容有:
| 項目 | 說明 |
| ----------------------- | ---------------------------------- |
| 欄位表格 | 看你都放了哪些欄位進去 |
| Indexes | 索引,用來加快查詢效率 |
| Foreign-key constraints | 自己的外鍵約束 |
| Referenced by | 被誰參照,也就是別人的外鍵約束 |
----
### psql 實際有哪些功能可以用?
忘記了沒關係,`help`幫你

像『\d』這個用法,就被註記在『\\?』區塊內,有非常多不見得會用到的功能:P
---
## Node 與 pg
----
### 回頭來說說js的連接
要在JS/TS中跟Postgres互動的話,一樣需要安裝對應的套件,指令如下:
```bash=
yarn add pg
yarn add -D @types/pg
```
分別是:
1. pg本體
2. pg的型別定義
----
### 接下來,就來寫點東西嘗試看看吧!
範例Code:
```javascript=
import pg from "pg";
const client = new pg.Client({
host: "127.0.0.1",
port: 5432,
database: "test",
user: "user",
password: "1234"
});
await client.connect();
const res = await client.query("SELECT version()");
console.log(res.rows[0]);
await client.end();
```
----
### 你出錯了嗎?
如果你看到以下的錯誤資訊:
```
(node:15046) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
```
不用擔心,這很正常,因為我們習慣使用的 import/export 是ES6引入的用法,在預設狀況下,Module System是CommonJS,以下擇一使用:
- <p style="color:lightgreen">修改副檔名為.mjs</p>
- <p style="color:orange">在package.json中加入『"type": "module"』</p>
----
### 再執行一次

如果你可以正常看到如圖的資訊,恭喜你,正常連接上去了!
----
### 讀取跟插入
拿前面的範例表來作為示範,讀取跟插入資料:
```javascript=
import pg from "pg";
import format from "pg-format";
const client = new pg.Client({
host: "127.0.0.1",
port: 5432,
database: "test",
user: "user",
password: "1234"
});
await client.connect();
await client.query("DELETE FROM student;");
await getRecord("Before:");
await client.query("INSERT INTO student VALUES ($1, $2, $3)", ["B10823027", "蘇祐民", 22]);
let data = [
["B11200001", "路人甲", 18],
["B10900001", "雲科老人", 21]
]
await client.query(format("INSERT INTO student VALUES %L", data));
await getRecord("After:");
await client.end();
async function getRecord(title) {
console.log(title);
const res = await client.query("SELECT * FROM student;");
res.rows.forEach(r => {
console.log(r);
})
}
```
----
### 又出錯了嗎?
如果你出現了這樣的錯誤資訊:
```
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'pg-format' imported from .../index.mjs
```
這是因為缺少了`pg-format`這個套件導致的
幫node安裝上就好:
```bash=
yarn add pg-format
yarn add -D @types/pg-format
```
----
### 見證成功的時刻

----
### 恭喜你,主要用法就是這樣!
除了透過Client的方式外,pg也支援以Pool的方式連接,有比較大量的操作需求,Pool也是比較合適的作法,這邊就不再多提,直接附上[使用文件](https://node-postgres.com/features/pooling)讓有興趣的同學可以自行研究!
----
### 我說那個Transaction呢?
交易一樣可以做到,可以理解為,你如何在psql中對資料庫操作,在pg套件的query指令,就照樣造句,只要確認你有該DB的權限,就能正常運作~
> [Transaction文件](https://node-postgres.com/features/transactions)
---
## TypeORM
----
### 什麼是ORM來著?
ORM的全名是,Object Relational Mapping,中文叫『物件關聯對映』,它所做的事,是在程式語言中,<span class="orange">虛擬出資料庫的物件</span>,將資料表格式當作類別定義,其中會有幾個Attribute作為主鍵,讓紀錄之間能做出區別,這樣一來,就不再需要所有操作都透過SQL語法的方式進行,ORM本身也有提供方法用來<span class="cyan">查詢</span>或是<span class="cyan">儲存</span>
----
### 那TypeORM呢?
TypeORM是JS/TS中一個還算好用的選項,雖然較為複雜,但在建構好專案後,可以方便的建構出複雜的查詢命令,目前在JS/TS生態系中,有三個大家比較常看到的選項,各有優劣,可以多嘗試看看~
<div style="background-color: white">
<image src="/_uploads/SJVg_rNST.png" width="250" /> <image src="/_uploads/r1MzOHNSp.png" width="250" /> <image src="/_uploads/HktE_S4r6.png" width="250" />
</div>
----
### TypeORM的優缺點?
- 基本ORM的方法
- QueryBuilder可以建立複雜查詢
- 有Migration機制可以處理結構
- 明確的相互關係(1 to N, N to 1, N to N)
- 支援多種資料庫(MySQL, MariaDB, PostgreSQL, MSSQL, Oracle, ...)
- 架構較為複雜
- 關聯定義比較不好理解
----
### 來嘗試看看吧
首先得安裝TypeORM,請你跟我這樣做:
```bash=
yarn add typeorm
yarn add reflect-metadata
```
----
### 接下來則是初始化時間
請輸入:
```bash=
yarn typeorm init
```

完成後,請幫我重新輸入:
```bash=
yarn install
```
----
### 使用VSCode開啟專案的時間
```bash=
code .
```

----
### 修改data-source
在src/data-source.ts中定義著用來連接資料庫的設定值,修改成與我們開在docker中的DB相同
```javascript=
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "user",
password: "1234",
database: "test",
...
})
```
----
### 調整資料表定義
在entity/User.ts中寫著範例的物件定義,讓我們來將其修改為前面的範例:
```typescript=
@Entity("student", { schema: "public" })
export class Student {
@PrimaryColumn("character varying", { name: "id", length: 9 })
id: string
@Column("character varying", { name: "name", length: 20, nullable: true })
name: string | null
@Column("integer", { name: "age", nullable: true })
age: number | null
}
```
----
### 接著修改data-source
將存放著entities的欄位調整成我們定義的Student
```javascript=
export const AppDataSource = new DataSource({
...,
synchronize: true,
logging: false,
entities: [Student],
migrations: [],
subscribers: [],
})
```
----
### index.ts有什麼好料?
在index.ts中,寫著實際對資料庫的操作,因此,讓我們來將想加入的內容轉換成TypeORM的程式碼:
```typescript=
import { AppDataSource } from "./data-source"
import { Student } from "./entity/Student"
AppDataSource.initialize().then(async () => {
console.log("Original Data:");
console.log(await AppDataSource.getRepository(Student).find());
console.log("Cleaning old Data...");
await AppDataSource.getRepository(Student).clear();
console.log("Inserting new student...");
const s = new Student();
s.id = "B10500001";
s.name = "老骨灰";
s.age = 25;
await AppDataSource.manager.save(s);
console.log("New Data:");
console.log(await AppDataSource.getRepository(Student).find());
}).catch(error => console.log(error))
```
----
### 執行的如何啦?

----
### 透過psql到資料庫也查詢一下

----
### 有沒有正常工作?
有錯誤也不要緊
可以回過頭來確認一下是否有哪裡寫錯了~
---
## 整合時間
----
### 還記得上週說到的整合嗎?
這次的範例,會包含Express, InversifyJS, TypeORM,會有點小複雜喔!
記得要將缺少的套件加進來喔~
```bash=
yarn add express inversify
yarn add -D @types/express
```
----
### 先來寫路由
```typescript=
import { inject, injectable } from "inversify";
import { db } from "./db";
@injectable()
export class route {
constructor(
@inject(db) private _db: db,
) {}
getRoot = async (req, res, next) => {
let data = await this._db.getStudent();
res.status(200).json({
status: true,
data
});
}
}
```
----
### 接著是DB的方法
```typescript=
import { EntityManager } from "typeorm";
import { getDataSource } from "./data-source";
import { Student } from "./entity/Student";
import { injectable } from "inversify";
@injectable()
export class db {
private manager: EntityManager;
constructor() {
this.manager = getDataSource().manager;
}
public async getStudent(): Promise<Student[]> {
const result = await this.manager.getRepository(Student).find();
return result;
}
}
```
----
### 建立DI容器
```typescript=
import { Container } from "inversify";
import { db } from "./db";
import { route } from "./routes";
const diContainer = new Container();
diContainer.bind<db>(db).to(db);
diContainer.bind<route>(route).to(route);
export { diContainer }
```
----
### data-source的細節
```typescript=
import "reflect-metadata"
import { DataSource } from "typeorm"
import { Student } from "./entity/Student"
let dataSource: DataSource;
export function createDataSource(): DataSource {
dataSource = new DataSource({
type: "postgres",
host: "127.0.0.1",
port: 5432,
username: "user",
password: "1234",
database: "test",
synchronize: true,
logging: false,
entities: [Student],
migrations: [],
subscribers: [],
});
return dataSource;
}
export function initDataSource(): Promise<DataSource> {
if (dataSource === undefined) {
throw new Error("You have to create DataSource first!");
}
return dataSource.initialize();
}
export function getDataSource(): DataSource {
return dataSource;
}
```
----
### Express 初始化
```typescript=
import express, { Express } from "express";
import { createDataSource, initDataSource } from "./data-source";
import { route } from "./routes";
import { diContainer } from "./di";
const app = express()
export default async function appInit(): Promise<Express> {
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
console.log("Parse JSON");
createDataSource();
await initDataSource();
console.log("DataSource Init");
let r = diContainer.get<route>(route);
app.use('/', r.getRoot);
console.log("Router Registed");
return app;
}
```
----
### 程式啟動器
```typescript=
import "reflect-metadata";
import { Express } from "express";
import appInit from "./app";
appInit().then((app: Express) => {
app.listen(3000, "0.0.0.0");
console.log('Service listening at: http://localhost:3000');
}).catch((err) => {
console.error(err);
});
```
----
### 測試時間!
首先啟動伺服器:
```bash=
yarn start
```
接著是送出請求:
```bash=
curl localhost:3000/
```
----
### 有得到一樣的結果嗎?

---
## 有問題嗎?
---
## 參考資料
----
- [GitHub - TypeORM](https://github.com/typeorm/typeorm)
- [GitHub - InversifyJS](https://github.com/inversify/InversifyJS)
- [node-postgres](https://node-postgres.com/)
---
# 掰掰~
<style>
.cyan {
color: cyan
}
.orange {
color: orange
}
</style>
{"title":"PostgreSQL with Express","description":"拿上週所講的Express當做基底","contributors":"[{\"id\":\"f8142aa2-66aa-4867-821d-2f1ffff7a7ba\",\"add\":12513,\"del\":25}]"}