iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

前言

前一天已經把 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?

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 開始使用前一天準備好的那個。

把 Database 設定移到 .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。

先讓 GET /notes 查到資料

與其一開始就把五個 API 全部改掉,我會先從 GET 開始。

因為只要 GET 成功,就代表至少這幾件事情都接起來了:Express Server 正常運作、Router 有找到 Controller、Controller 能取得 Repository 的資料,而 Repository 也真的查到了 PostgreSQL。

routes/notes.js

Router 還是做前面學過的事情,只負責把 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.js

Controller 前面已經學過,所以這裡也不需要重新介紹。它接到 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。

Repository:SQL 開始有自己的位置

如果把 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:把 Router 掛回來

最後 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。

先測試 GET,再完成其他 CRUD

現在可以先啟動 Server:

npm run dev

接著用瀏覽器或 Postman:

GET http://localhost:3000/notes

如果可以看到 Day 15 已經建立好的 notes 資料,就代表 Express 已經成功查到 PostgreSQL。

這時候可以再做一個很有感的測試:新增一筆 Note,關掉 Node.js Server,重新啟動,再呼叫一次:

GET /notes

如果資料還在,就代表它已經不是存在 JavaScript Array 裡,而是真的被 PostgreSQL 保存下來了。

這也是今天和前幾天最大的差別。

把原本的 Array CRUD 換掉

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,現在先不加入。等之後真的開始出現比較複雜的商業邏輯,再來處理會比較自然。

那 ORM 呢?

寫到這裡,應該會開始覺得 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 到底解決了什麼問題。


上一篇
Day 15|從 SQL 到真正的 Database:開始認識 PostgreSQL
下一篇
Day 17|讓資料庫操作變得更簡單:ORM
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言