今天我們來開發這套系統最常被呼叫的 API:取得持有紀錄列表。
還記得 DAY 7 我們把資料正規化成 pokemon_base (基礎圖鑑) 與 user_pokemon_holdings (個人持有紀錄) 兩張表嗎?現在,我們必須在後端用 SQL 的 JOIN 語法,把分離的資料縫合起來,變成一張完整的卡片資訊丟給前端。
在 src/routes/holdings.ts 中,我們撰寫了這段 JOIN 查詢:
router.get('/', async (req, res) => {
// 從昨天的 Middleware 取得登入者的資訊
const userId = req.user.userId;
try {
// 使用 JOIN 把兩張表的資料拼起來
const result = await db.execute({
sql: `
SELECT
h.holding_id,
h.player_id,
h.gender,
h.is_shiny,
b.pokemon_id,
b.name_zh,
b.generation
FROM user_pokemon_holdings h
-- 核心:透過 pokemon_id 把圖鑑表的資料拉過來
JOIN pokemon_base b ON h.pokemon_id = b.pokemon_id
WHERE h.player_id = ?
ORDER BY b.pokemon_id ASC, h.created_at DESC
`,
args: [userId]
});
res.json(result.rows);
} catch (error) {
res.status(500).json({ error: '讀取紀錄失敗' });
}
});
JOIN 語法的效能考量許多新手一聽到 JOIN 就會擔心:「把兩張表綁在一起搜,效能會不會很差?」
其實,關聯式資料庫對 JOIN 的優化已經非常成熟了,但前提是**「您有沒有建對 Index (索引)」**!
pokemon_base.pokemon_id 是 Primary Key,預設就有索引。當資料庫在執行 JOIN ... ON h.pokemon_id = b.pokemon_id 時,它能像查字典一樣,用極快的速度瞬間找到對應的圖鑑資料,即使有幾百萬筆紀錄也能在幾毫秒內回傳!所以,大膽且正確地使用 JOIN 吧!

在 Postman 中帶上 Bearer Token,發送 GET 請求到
/api/holdings,成功撈出包含name_zh與is_shiny等關聯資料的 JSON 陣列。
JOIN 成功把我們的資料立體化了!能夠「讀」出資料後,明天我們要來實作「寫入」的功能,並且探討一個極為致命的資料庫安全問題:SQL Injection!