### 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" ``` ---- ### 執行一下 ![docker compose](https://hackmd.io/_uploads/HJwLkQNHp.png) ---- ### 確認一下是否運作了 ![docker ps](https://hackmd.io/_uploads/rkwexmNra.png) ---- ### 如何透過CLI工具連進資料庫? Postgres有提供一系列工具用來進行管理,最常使用的,是`psql`,以下提供範例: ```bash= docker compose exec -it db psql ``` ---- ### 你出錯了嗎~ ![psql error](https://hackmd.io/_uploads/rJdVZQNST.png) 出錯的原因很簡單,psql會以容器內的『預設使用者』嘗試進行連線,但發現找不到,於是乎就出現錯誤了 ---- ### 來補上缺少的參數 ```bash= docker compose exec -it db psql -U user -W test ``` 小小解說: - -U:指定使用者 - -W:輸入密碼 - -h:指定伺服器IP或Hostname - -p:指定伺服器的Port - test:這是資料庫名稱,要跟你設定的compose內的名字相同喔 ---- ### 應該連接上去了 ![psql success](https://hackmd.io/_uploads/Hy7XG7NHp.png) ---- ### 取得基本資訊 ![psql system info](https://hackmd.io/_uploads/Sk174QErT.png =800x) 其他可用的資訊,可以參考[官方文件說明](https://www.postgresql.org/docs/16/functions-info.html) ---- ### 還記得前面設定的資料庫名稱嗎? ![db name](https://hackmd.io/_uploads/rkDGHmVH6.png) 若你還沒注意到,可以看一下『=#』前面的字串,正常來說,就會是你設定的資料庫名稱! ---- ### 如何查看資料庫內的資料表? 在MySQL或MariaDB中,通常是show tables; 但在psql中,有更簡單的方法,靠『\d』就好! ![\d not found](https://hackmd.io/_uploads/SyIZLmES6.png) 正常應該會像圖片的資訊,顯示沒有Relation ---- ### 來新增資料表試試 如果你對標準SQL語法有概念,就太好了,大部分的用法都是通用的喔! ```sql= create table student( id varchar(9) primary key, name varchar(20), age int ); ``` ---- ### 看圖說故事 ![create table](https://hackmd.io/_uploads/r155P7NSa.png) ---- ### 如果我想看表的定義可以怎麼做? 你可能會很訝異,但一樣可以用『\d』,加上表名就好! ![\d student](https://hackmd.io/_uploads/rkBTu74ra.png) ---- ### 有關表的定義 剛剛的那張圖,因為沒有複雜的關係,所以沒有太多資訊,但基本上會顯示的內容有: | 項目 | 說明 | | ----------------------- | ---------------------------------- | | 欄位表格 | 看你都放了哪些欄位進去 | | Indexes | 索引,用來加快查詢效率 | | Foreign-key constraints | 自己的外鍵約束 | | Referenced by | 被誰參照,也就是別人的外鍵約束 | ---- ### psql 實際有哪些功能可以用? 忘記了沒關係,`help`幫你 ![help](https://hackmd.io/_uploads/BJDlj7VrT.png) 像『\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> ---- ### 再執行一次 ![js pg version](https://hackmd.io/_uploads/By8ZEVVS6.png) 如果你可以正常看到如圖的資訊,恭喜你,正常連接上去了! ---- ### 讀取跟插入 拿前面的範例表來作為示範,讀取跟插入資料: ```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 ``` ---- ### 見證成功的時刻 ![data inserted](https://hackmd.io/_uploads/SyTxsNNSp.png) ---- ### 恭喜你,主要用法就是這樣! 除了透過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 ``` ![init success](https://hackmd.io/_uploads/r1VBarNBa.png) 完成後,請幫我重新輸入: ```bash= yarn install ``` ---- ### 使用VSCode開啟專案的時間 ```bash= code . ``` ![vscode](https://hackmd.io/_uploads/Hyu6-LVBa.png) ---- ### 修改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)) ``` ---- ### 執行的如何啦? ![yarn start](https://hackmd.io/_uploads/HJHZrdEBT.png) ---- ### 透過psql到資料庫也查詢一下 ![psql select](https://hackmd.io/_uploads/S1-YrOVra.png) ---- ### 有沒有正常工作? 有錯誤也不要緊 可以回過頭來確認一下是否有哪裡寫錯了~ --- ## 整合時間 ---- ### 還記得上週說到的整合嗎? 這次的範例,會包含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/ ``` ---- ### 有得到一樣的結果嗎? ![request success](https://hackmd.io/_uploads/ry3qKtVrT.png) --- ## 有問題嗎? --- ## 參考資料 ---- - [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}]"}
    281 views
   Owned this note