iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
JavaScript

從看不懂到做出來,用 PawPal 走過前端新手村系列 第 7

Day 7|表單、API 與資料庫:欄位沒有統一,資料就對不上

  • 分享至 

  • xImage
  •  

今天的故事

上一篇提到,我第一次看到寵物資料從資料庫經過後端 API,最後顯示在前端卡片上。

那一刻真的很有成就感。

原本以為資料已經成功出現在畫面上,代表寵物功能最困難的部分應該結束了。

但很快我就發現,事情沒有這麼順利。

有些欄位可以正常顯示,有些欄位卻是空的;表單裡明明已經填了資料,送出後卻沒有按照預期保存。

當時我第一個反應是:

  • API 是不是壞了?
  • 資料庫是不是沒有寫進去?
  • 前端是不是沒有取得 Response?
  • 還是我又漏掉哪一段程式?

後來我才逐漸理解,資料即使已經送出或回傳,如果前端、後端和資料庫的欄位名稱沒有明確對應,保存或顯示仍會出現異常。

Day 7 要處理的核心是:

前端、後端和資料庫使用的欄位名稱沒有完全一致。

對人來說,這些欄位看起來都在表示同一件事。

但對程式來說,只要名稱不一樣,就是不同的資料。


同一個欄位,為什麼會有不同名字?

在 JavaScript 和 Vue 裡,我們通常會使用 camelCase 命名。

例如:

microchipNumber
bloodType
furColor
photoUrl

camelCase 的特色,是第一個單字使用小寫,後面的單字開頭使用大寫。

例如:

microchip + number
↓
microchipNumber

但在 PostgreSQL 資料庫裡,欄位通常會使用 snake_case

例如:

microchip_number
blood_type
fur_color
avatar_url

snake_case 會使用底線把單字分開。

這兩種命名方式本身都沒有錯。

問題是,當資料要從前端送到後端,再寫入資料庫時,程式必須知道它們之間的對應關係。

例如:

前端:microchipNumber
後端:microchip_number
資料庫:microchip_number

如果沒有做轉換,後端接收到的是:

{
  microchipNumber: '123456789'
}

但後端真正要讀取的是:

req.body.microchip_number

這時候取得的結果就會是:

undefined

資料明明存在於 Request 裡,但因為程式讀取了不同的 key,所以看起來就像資料沒有傳過來。


Object 的 key 必須完全一致

以前學習 JavaScript 物件時,我知道可以透過 key 取得 value。

例如:

const pet = {
  name: 'Milk',
  microchipNumber: '123456789',
}

取得名字時可以寫:

pet.name

取得晶片號碼時可以寫:

pet.microchipNumber

但如果寫成:

pet.microchip_number

結果會是:

undefined

因為物件裡根本沒有叫做 microchip_number 的 key。

雖然 microchipNumbermicrochip_number 對人來說意思一樣,但 JavaScript 不會自動幫我們判斷它們是同一個欄位。

程式只會確認:

這個 key 是否完全相同?

這也是我第一次真正理解,欄位名稱不是只有「看起來差不多」就可以。

只差一個底線、大小寫或單複數,程式就會讀不到資料。


PawPal 需要處理的欄位差異

在 PawPal 的寵物功能裡,前端和後端之間有幾個需要特別處理的欄位。

例如:

前端欄位 後端/資料庫欄位
microchipNumber microchip_number
bloodType blood_type
furColor fur_color
photoUrl avatar_url
note notes

前面三組比較容易理解,只是 camelCase 和 snake_case 的差別。

但後面兩組不只是命名格式不同,連單字本身也不完全一樣:

photoUrl ↔ avatar_url
note ↔ notes

如果只是把大寫字母換成底線,這兩個欄位仍然無法正確對應。

所以欄位轉換不能只靠猜測,也不能假設所有欄位都能使用同一種自動規則處理。

photoUrl 是前端接收與使用寵物資料時的欄位;現行新增表單選取圖片後,會傳出 avatarFile,再由 API 層以 avatar 作為檔案欄位送出。


表單有資料,不代表 API 收到的格式正確

以下不是現行新增寵物表單的逐字資料結構,而是為了說明欄位 mapping,使用一個不包含圖片檔案上傳流程的簡化 JSON 範例:

const form = {
  name: 'Milk',
  microchipNumber: '123456789',
  bloodType: 'A',
  furColor: '灰色',
  photoUrl: 'https://example.com/milk.jpg',
  note: '容易緊張',
}

從前端的角度看,資料都存在。

在畫面上輸入時,也不一定會出現問題。

但如果直接把這個物件送給後端:

await axios.post('/api/v1/pets', form)

後端實際讀取的欄位是:

{
  name: 'Milk',
  microchip_number: '123456789',
  blood_type: 'A',
  fur_color: '灰色',
  avatar_url: 'https://example.com/milk.jpg',
  notes: '容易緊張',
}

這兩個物件看起來很像,實際上卻包含不同的 key。

所以「表單裡有資料」只能證明前端狀態中存在這些內容。

它不能直接證明:

  • Request Payload 的格式正確
  • 後端讀取到了正確欄位
  • 資料庫成功保存資料
  • API 回傳的欄位能被前端正確讀取

這也是我當時一直卡住的原因。

我只看到畫面上已經有資料,就以為後面的流程應該也會知道這些資料代表什麼。

但程式不會自己理解欄位之間的關係。

我們必須明確告訴它怎麼轉換。


一開始最容易做的方式:每次送出前自己轉換

最直接的做法,是在表單送出前建立一個新的物件。

下面是簡化後的概念範例:

const payload = {
  name: form.name,
  microchip_number: form.microchipNumber,
  blood_type: form.bloodType,
  fur_color: form.furColor,
  avatar_url: form.photoUrl,
  notes: form.note,
}

await axios.post('/api/v1/pets', payload)

這樣後端就能收到自己期待的欄位名稱。

這個方式在功能比較少時看起來沒有問題。

但當新增、編輯和圖片更新都需要處理寵物資料時,相同的轉換就會散落在很多地方。

例如:

AddPetModal
PetProfileModal
Pet Store
其他寵物元件

每個地方都自己轉一次,久了很容易出現:

  • 有一個元件忘記轉換某個欄位
  • 新增功能和編輯功能使用不同命名
  • 同一個欄位在不同檔案裡寫法不一致
  • 後端修改欄位後,需要同時修改很多元件
  • Debug 時不知道資料在哪一層被改過

當時的我只想讓功能先動起來,不一定會立刻想到維護問題。

但隨著寵物功能越做越多,就會開始感受到重複轉換帶來的混亂。


PawPal 把欄位轉換集中到 API 層

PawPal 的寵物資料欄位轉換集中在:

src/api/pet.js

前端元件可以繼續使用比較符合 JavaScript 習慣的 camelCase。

API 層則負責把資料轉換成後端需要的格式。

實際的 Request 轉換函式名稱是 mapPetToApi。以下保留與本篇主題相關的簡化示意:

const mapPetToApi = (pet) => ({
  name: pet.name,
  microchip_number: pet.microchipNumber,
  blood_type: pet.bloodType,
  fur_color: pet.furColor,
  avatar_url: pet.photoUrl,
  notes: pet.note,
})

送出資料時:

const payload = mapPetToApi(form)

await axios.post('/api/v1/pets', payload)

上面的 Axios 範例只聚焦欄位轉換;PawPal 實作會從 localStorage 取得 pawpal_token,並在請求附上 Authorization: Bearer <token>

實際新增時,createPetRequestData 會先使用 mapPetToApi:沒有 avatarFile 時送出 JSON;有圖片檔案時改用 FormData,並以 avatar 作為檔案欄位。

這樣元件不需要知道資料庫使用哪些欄位名稱。

它只需要處理前端畫面真正需要的資料。

資料流會變成:

Vue 表單
使用 camelCase
↓
src/api/pet.js
統一進行欄位轉換
↓
Express API
使用後端欄位格式
↓
PostgreSQL
使用 snake_case

我後來才理解,API 檔案不只是集中放 Axios 請求。

它也可以成為前端和後端之間的翻譯層。


資料回來時,也需要轉換

欄位轉換不只發生在「送出資料」的時候。

當後端從資料庫取得資料並回傳給前端時,API 層也需要補上前端習慣使用的 camelCase 欄位。

後端回傳的資料例如:

{
  name: 'Milk',
  microchip_number: '123456789',
  blood_type: 'A',
  fur_color: '灰色',
  avatar_url: 'https://example.com/milk.jpg',
  notes: '容易緊張',
}

但前端元件讀取的是:

pet.microchipNumber
pet.bloodType
pet.furColor
pet.photoUrl
pet.note

如果沒有整理這些欄位,畫面仍然可能會顯示空白。

下面同樣是簡化後的概念範例:

const mapPetFromApi = (pet) => ({
  ...pet,
  microchipNumber: pet.microchip_number,
  bloodType: pet.blood_type,
  furColor: pet.fur_color,
  photoUrl: pet.avatar_url,
  note: pet.notes,
})

取得資料後:

const response = await axios.get('/api/v1/pets')

const pets = response.data.pets.map(mapPetFromApi)

這樣前端其他地方就能統一使用 camelCase。

也就是說,API 層需要處理兩個方向:

送出資料
camelCase → 後端欄位格式

取得資料
後端欄位格式
↓
API 層整理
↓
前端使用 camelCase

為什麼不直接讓所有地方使用相同命名?

看到這裡,可以問一個問題:

為什麼不乾脆讓前端、後端和資料庫全部使用相同欄位名稱?

如果專案一開始就能統一,確實可以減少很多轉換。

但實際開發時,不同環境有自己的命名習慣:

  • JavaScript 常使用 camelCase
  • PostgreSQL 常使用 snake_case
  • API 需要維持既有格式
  • 舊功能已經使用另一套名稱
  • 團隊不同成員在不同時間建立欄位

而且像:

photoUrl ↔ avatar_url
note ↔ notes

這種情況不是單純改大小寫就能解決。

所以真正重要的不是強迫每一層長得一模一樣,而是:

必須清楚決定誰負責轉換,而且轉換規則只能有一個主要來源。

在 PawPal 裡,把這件事集中在 src/api/pet.js,就能避免每個 Vue 元件各自處理。


我是怎麼一步一步排查的?

當時遇到資料沒有正常顯示時,我一開始沒有完整的排查順序。

很容易看到畫面是空的,就開始到處修改程式。

後來我慢慢整理出一個比較清楚的檢查方式。

1. 先確認表單狀態

先確認使用者輸入後,前端物件裡是不是真的有資料。

例如:

console.log(form)

要確認的不是只有 value,還要注意 key:

{
  microchipNumber: '123456789'
}

2. 查看 Network 的 Request Payload

表單有資料,不代表送出去的資料也正確。

在瀏覽器開發者工具的 Network 裡,可以查看 Request Payload。

確認送出的內容是:

microchip_number

還是:

microchipNumber

這一步可以幫助判斷,問題發生在送出 API 之前,還是後端收到資料之後。


3. 查看 API Response

接著確認 API 回傳了什麼。

例如後端回傳:

{
  microchip_number: '123456789'
}

但前端卻讀取:

pet.microchipNumber

這時就能知道,資料其實已經回來了,只是前端使用了不同的 key。


4. 確認後端實際讀取的欄位

如果 Request Payload 看起來正常,就繼續檢查後端。

例如後端是讀取:

req.body.microchip_number

還是:

req.body.microchipNumber

前端送出的 key 和後端讀取的 key 必須一致,否則後端就會取得 undefined


5. 確認資料庫實際保存的內容

最後再到資料庫確認:

  • 這筆資料是否真的存在
  • 寫進了哪個欄位
  • 欄位內容是不是 NULL
  • 更新時是否修改到正確資料

這樣才能分辨問題到底發生在:

表單
↓
Request
↓
後端
↓
資料庫
↓
Response
↓
前端畫面

不要只看錯誤訊息,也要看資料長什麼樣子

這次問題讓我學到一件很重要的事。

有些 Bug 不一定會出現明顯的紅色錯誤訊息。

程式沒有當掉,API 甚至也回傳成功。

但資料就是沒有顯示,或某些欄位一直是空的。

這時候真正需要確認的是:

  • 每一層的資料長什麼樣子
  • key 有沒有改變
  • value 在哪一層消失
  • 哪個地方負責轉換

以前遇到問題時,我常常只看畫面最後有沒有成功。

後來才知道,比起一直猜原因,更有效的方式是沿著資料流一層一層確認。


如果現在重新做一次

如果現在重新處理同樣的功能,我會在開發前先整理一份欄位對照表。

例如:

畫面用途 前端欄位 API/資料庫欄位
晶片號碼 microchipNumber microchip_number
血型 bloodType blood_type
毛色 furColor fur_color
寵物照片 photoUrl avatar_url
備註 note notes

接著明確決定:

所有欄位轉換都由 src/api/pet.js 負責

元件只處理:

  • 使用者輸入
  • 表單狀態
  • Loading
  • 成功與失敗提示
  • 畫面更新

API 層則處理:

  • API 路徑
  • HTTP 方法
  • Request Payload
  • 欄位轉換
  • Response 資料整理

這樣遇到欄位問題時,就不用在很多元件之間來回尋找。


錯誤沒有消失,但我比較知道怎麼面對

剛開始寫專案時,只要畫面沒有顯示資料,我就會覺得問題很嚴重。

有時候甚至不知道應該從哪一個檔案開始看。

但這次欄位對不上的問題,讓我第一次建立了比較具體的排查方向。

我會開始問:

表單裡有資料嗎?
↓
Request 送出了什麼?
↓
後端讀取哪個欄位?
↓
資料庫保存在哪裡?
↓
Response 回傳什麼?
↓
前端最後讀取哪個 key?

錯誤並沒有因此消失。

之後還是會遇到新的 Bug,也還是會卡住。

但至少我不再只是看到畫面不對,就立刻到處修改程式。

我開始知道,可以沿著資料流一步一步縮小問題範圍。


今天的學習整理

這次欄位對應問題,讓我整理出幾個重點:

  1. JavaScript 物件的 key 必須完全一致。
  2. microchipNumbermicrochip_number 對程式來說是不同欄位。
  3. 表單有資料,不代表 API 收到的格式正確。
  4. API 有回傳資料,不代表前端讀取的 key 正確。
  5. camelCasesnake_case 可以同時存在,但必須有清楚的轉換規則。
  6. photoUrlavatar_url 這類欄位不能只靠自動改大小寫處理。
  7. 欄位轉換集中在 API 層,比散落在不同元件裡更容易維護。
  8. Debug 時要沿著表單、Request、後端、資料庫、Response 和畫面逐層確認。
  9. 沒有錯誤訊息,不代表資料流程完全正確。

這次我學到的不只是兩種命名方式。

更重要的是,我開始知道一筆資料經過不同層時,格式會改變,而每一次改變都需要有人負責。


下一篇預告

欄位名稱統一後,寵物資料終於可以正確送進後端。

但新增成功,不代表畫面會自動出現新的寵物資料。前端還需要重新取得列表,並把 API 回傳的資料顯示成寵物卡片。

Day 8,我會從「新增」到「顯示」,整理寵物資料如何透過 POST、GET 與前端畫面完成一段完整的資料流程。

下一篇:

Day 8|從「新增」到「顯示」,我的第一個 CRUD 完成了!


上一篇
Day 6|第一次接手寵物功能,真的沒有想像中簡單
下一篇
Day 8|從「新增」到「顯示」,我的第一個 CRUD 完成了!
系列文
從看不懂到做出來,用 PawPal 走過前端新手村12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言