前一天已經把 PostgreSQL 建好了,也建立了這次會用到的 Database 和 notes Table。做到這裡,資料庫本身已經可以工作,但前面寫好的 Express API 還是和它沒有關係,Notes 也仍然放在 JavaScript 的 Array 裡。
之前為了先把 API 的基本流程弄懂,這樣做其實很合理。GET 就從 Array 裡拿資料,POST 就新增一筆,PATCH 和 DELETE 也是直接修改 Array。問題在於,這些資料只存在 Node.js 執行的這段時間,只要 Server 重開,原本的資料就消失了。
所以今天要做的事情很單純,就是把這個 Array 換掉,讓 Express 開始使用昨天建立好的 PostgreSQL。順便也會遇到一個很實際的問題:既然現在真的要連 Database,那連線資訊應該放在哪裡?SQL 又該寫在哪裡?
這一天就從「讓 Express 第一次查到 PostgreSQL 裡的資料」開始。
前面已經學過 Router 和 Controller,這次不需要整個專案重新來過,只要增加幾個和 Database 有關的檔案。
project/
├── app.js
├── db.js
├── routes/
│ └── notes.js
├── controllers/
│ └── notes.js
├── repositories/
│ └── notes.js
├── .env
├── .gitignore
└── package.json
今天第一次看到的主要是 db.js、repositories/notes.js 和 .env。前兩個是因為開始接 Database 才需要,.env 則是拿來放 Database 的連線設定。
先不用把這個結構全部背起來,跟著做一次,之後就會慢慢知道每個檔案為什麼要存在。
Node.js 本身不會直接處理 PostgreSQL,所以需要另外安裝 PostgreSQL Driver。這次使用的是 pg,它就是 Node.js 和 PostgreSQL 之間負責溝通的工具。
先安裝:
npm install pg
安裝完成之後,就可以開始建立 Database Connection。
db.js既然之後不只一個 API 會用到 Database,就不要讓每支 API 都自己處理一次連線,可以先把這件事集中起來。
在專案根目錄建立 db.js:
project/
├── app.js
├── db.js ← 新增
├── routes/
│ └── notes.js
├── controllers/
│ └── notes.js
├── repositories/
│ └── notes.js
├── .env
├── .gitignore
└── package.json
這裡使用 pg 提供的 Pool:
import "dotenv/config";
import pg from "pg";
const { Pool } = pg;
const pool = new Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
export default pool;
這裡的 Pool 可以先理解成管理資料庫連線的工具。API Server 會一直收到 Request,如果每次都重新建立一條 Database Connection,查完再關掉,會一直重複做相同的事情,所以通常會使用 Connection Pool 來管理這些連線。
另外,DB_NAME 要填的是 Day 15 已經建立好的 Database。今天不是重新建立一個新的 Database,而是讓 Express 開始使用前一天準備好的那個。
.env剛才如果把密碼直接寫在 db.js:
password: "your_password"
雖然本機可以正常執行,但這種寫法之後很容易遇到問題。程式碼可能會放進 Git,不同環境也可能連到不同的 Database,如果所有資訊都寫死在程式裡,每次換環境都要修改程式碼。
所以先在專案根目錄建立 .env:
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=你的PostgreSQL密碼
DB_NAME=Day15建立的Database名稱
接著安裝 dotenv:
npm install dotenv
在 db.js 的最上面加入:
import "dotenv/config";
這樣就可以透過 process.env 取得 .env 裡的內容:
process.env.DB_HOST
process.env.DB_PASSWORD
process.env.DB_NAME
所以剛才的 db.js 就不需要把帳號密碼直接寫進去。
因為 .env 可能包含 Database 密碼,所以記得在 .gitignore 加上:
node_modules/
.env
到這裡,Database Connection 的設定就處理完了,接下來才真正開始改 API。
與其一開始就把五個 API 全部改掉,我會先從 GET 開始。
因為只要 GET 成功,就代表至少這幾件事情都接起來了:Express Server 正常運作、Router 有找到 Controller、Controller 能取得 Repository 的資料,而 Repository 也真的查到了 PostgreSQL。
routes/notes.jsRouter 還是做前面學過的事情,只負責把 API 路徑接到 Controller:
import express from "express";
import {
getNotes,
getNoteById,
createNote,
updateNote,
deleteNote,
} from "../controllers/notes.js";
const router = express.Router();
router.get("/", getNotes);
router.get("/:id", getNoteById);
router.post("/", createNote);
router.patch("/:id", updateNote);
router.delete("/:id", deleteNote);
export default router;
目前不用改 Router 的概念,只是把 Controller 後面的資料來源從 Array 換成 Database。
controllers/notes.jsController 前面已經學過,所以這裡也不需要重新介紹。它接到 Request 後,去取得資料,再把結果回傳出去。
import {
findAllNotes,
findNoteById,
insertNote,
updateNoteById,
deleteNoteById,
} from "../repositories/notes.js";
export async function getNotes(req, res, next) {
try {
const notes = await findAllNotes();
res.json(notes);
} catch (error) {
next(error);
}
}
export async function getNoteById(req, res, next) {
try {
const note = await findNoteById(req.params.id);
res.json(note);
} catch (error) {
next(error);
}
}
export async function createNote(req, res, next) {
try {
const note = await insertNote(
req.body.title,
req.body.content
);
res.status(201).json(note);
} catch (error) {
next(error);
}
}
export async function updateNote(req, res, next) {
try {
const note = await updateNoteById(
req.params.id,
req.body.title,
req.body.content
);
res.json(note);
} catch (error) {
next(error);
}
}
export async function deleteNote(req, res, next) {
try {
const note = await deleteNoteById(req.params.id);
res.json(note);
} catch (error) {
next(error);
}
}
這時候 Controller 裡已經沒有 SQL。因為資料庫的事情,接下來交給 Repository。
如果把 SQL 直接寫在 Controller 裡,當然也能跑。問題是現在只有一個 notes Table,還沒什麼感覺;之後 API 越來越多,Controller 裡就會慢慢出現一大堆 SQL。
所以這次先把資料庫操作集中到 repositories/notes.js。
建立:
repositories/
└── notes.js
然後把 SQL 放進去:
import pool from "../db.js";
export async function findAllNotes() {
const result = await pool.query(
"SELECT * FROM notes ORDER BY created_at DESC"
);
return result.rows;
}
export async function findNoteById(id) {
const result = await pool.query(
"SELECT * FROM notes WHERE id = $1",
[id]
);
return result.rows[0];
}
export async function insertNote(title, content) {
const result = await pool.query(
`
INSERT INTO notes (title, content)
VALUES ($1, $2)
RETURNING *
`,
[title, content]
);
return result.rows[0];
}
export async function updateNoteById(id, title, content) {
const result = await pool.query(
`
UPDATE notes
SET title = $1,
content = $2
WHERE id = $3
RETURNING *
`,
[title, content, id]
);
return result.rows[0];
}
export async function deleteNoteById(id) {
const result = await pool.query(
"DELETE FROM notes WHERE id = $1 RETURNING *",
[id]
);
return result.rows[0];
}
現在就可以很直覺地看到 SQL 都集中在同一個地方。Controller 不需要知道 PostgreSQL 的語法,只需要呼叫 findAllNotes() 或 insertNote() 之類的函式。
這就是 Repository 最基本的用途:把資料庫操作整理到一起。
現在先停在 Controller → Repository 就好,Service 暫時不需要加入。因為目前的 Note API 還沒有複雜的商業邏輯,如果只是 Controller 呼叫 Repository,中間再多一個 Service,反而只是多一層。等真的遇到需要處理比較複雜的功能時,再加入 Service 會更容易理解它的價值。
$1、$2 是什麼?Repository 裡會一直看到這種寫法:
const result = await pool.query(
"SELECT * FROM notes WHERE id = $1",
[id]
);
第一次看到 $1 可能會有點奇怪,其實它就是一個參數的位置。
$1
↓
第一個參數
所以:
"SELECT * FROM notes WHERE id = $1"
[id]
就是把 id 傳進 $1。
如果有兩個參數:
const result = await pool.query(
"SELECT * FROM notes WHERE title = $1 AND id = $2",
[title, id]
);
那 $1 對應 title,$2 對應 id。
這種方式叫做參數化查詢。不要把使用者輸入直接拼進 SQL:
const result = await pool.query(
`SELECT * FROM notes WHERE id = ${id}`
);
而是把 SQL 和資料分開:
const result = await pool.query(
"SELECT * FROM notes WHERE id = $1",
[id]
);
這會是之後寫 Database Query 時非常基本的一個習慣。
最後 app.js 還是維持我們原本熟悉的角色,不需要知道 SQL 長什麼樣子,也不需要自己處理 Database Connection。
import express from "express";
import notesRouter from "./routes/notes.js";
const app = express();
app.use(express.json());
app.use("/notes", notesRouter);
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({
status: "error",
message: "Internal Server Error",
});
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
到這裡,整個專案的結構就會很清楚:
app.js
↓
routes/notes.js
↓
controllers/notes.js
↓
repositories/notes.js
↓
db.js
↓
PostgreSQL
而且每一段程式碼都有自己的位置,不需要全部塞進 app.js。
現在可以先啟動 Server:
npm run dev
接著用瀏覽器或 Postman:
GET http://localhost:3000/notes
如果可以看到 Day 15 已經建立好的 notes 資料,就代表 Express 已經成功查到 PostgreSQL。
這時候可以再做一個很有感的測試:新增一筆 Note,關掉 Node.js Server,重新啟動,再呼叫一次:
GET /notes
如果資料還在,就代表它已經不是存在 JavaScript Array 裡,而是真的被 PostgreSQL 保存下來了。
這也是今天和前幾天最大的差別。
GET 成功之後,剩下的 CRUD 就只是把資料操作一個一個換掉。
原本:
GET
→ Array
POST
→ Array.push()
PATCH
→ 修改 Array
DELETE
→ Array.splice()
現在:
GET
→ SELECT
POST
→ INSERT
PATCH
→ UPDATE
DELETE
→ DELETE
API 的路徑本身並沒有因此改變:
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
改變的只是 API 背後怎麼取得和保存資料。
以前資料在:
Node.js
↓
Array
現在則是:
Node.js
↓
PostgreSQL
所以 Server 可以重新啟動,資料卻不會跟著消失。
做到這裡,其實會發現接 Database 不只是多學幾個 SQL 而已,還會開始遇到「程式到底該放在哪裡」這個問題。
以前 API 很小,把東西放在一起還不會覺得怎麼樣;現在多了 PostgreSQL,就有 Connection、SQL 和資料處理,如果全部塞在同一個檔案裡,很快就會開始難找。
所以這次先做了一個很簡單的整理:Router 照樣處理路徑,Controller 處理 HTTP,Repository 集中 Database 操作,db.js 則專門建立 PostgreSQL Connection Pool。
這不是什麼一定要遵守的規則,只是當程式開始變大之後,一種讓自己比較容易找到東西的方法。
至於 Service,現在先不加入。等之後真的開始出現比較複雜的商業邏輯,再來處理會比較自然。
寫到這裡,應該會開始覺得 SQL 好像慢慢變多了。
查資料要寫 SELECT,新增要寫 INSERT,修改要寫 UPDATE,刪除又要寫 DELETE。現在只有一張 notes Table 還好,等資料表越來越多,Repository 裡的 SQL 也會越來越多。
於是下一個問題就出現了:
既然 SQL 已經可以操作資料庫,為什麼還有人需要 ORM?
這就是下一篇要處理的事情。
這一天先直接使用 pg 和 SQL,是因為先把「Node.js 怎麼真的操作 PostgreSQL」走過一次。等知道這些 SQL 到底在做什麼之後,再來看 ORM 幫我們把哪些事情包起來,會更容易理解。
前一天建立好的 Database,今天終於和 Express 接上了。
原本 Notes 放在 Array 裡,Server 一重開就消失,現在資料改放進 PostgreSQL,API 可以重新啟動,資料也還留著。過程中也開始把程式碼分開,讓 Database Connection 放到 db.js,SQL 放到 Repository,Controller 則繼續處理 HTTP。
一路做到這裡,原本看起來各自獨立的東西開始接在一起:
Client
↓
Express
↓
Controller
↓
Repository
↓
PostgreSQL
前幾天是在學怎麼把 API 做出來,今天則是讓 API 背後真的有資料可以保存。
以前 Server 重開,資料就跟著消失;從今天開始,資料終於有地方可以留下來。
下一篇,就來看看 SQL 已經可以操作資料庫了,為什麼還需要 ORM,以及 ORM 到底解決了什麼問題。