Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

API 批次請求:為什麼你的函式應該從「一組」開始設計?

最後更新:2026年4月28日Web API

你有沒有遇過這種情況:一開始寫好的函式跑得好好的,但需求一變多,整個程式碼就要砍掉重練?

這篇文章要聊的,就是一個很小但影響很大的設計決定——你的函式要處理「一個」還是「一組」?

單筆處理函式:從一個 ID 到一個結果

假設你在做一個後台系統,有一個功能是「查詢使用者資料」。

流程很單純:接收一個使用者 ID,拿這個 ID 去打外部 API 取得原始資料,做一些處理(像是格式轉換、過濾不需要的欄位),然後把結果回傳。

你大概會寫出這樣的東西:

processUser(id):
  data = callAPI(id)
  result = transform(data)
  return result

這段程式碼完全能動,沒有 bug,邏輯也很清楚。

測試跑過,上線沒問題,你心滿意足地收工。

然後某天,需求變了——不是處理一個使用者,是一百個。

你的直覺反應大概是這樣:

results = []
for each id in hundredIds:
  results.push(processUser(id))

看起來合理,對吧?

能動,也沒有 bug。

但問題來了。

用 for 迴圈打 100 次 API 的效能代價

用 for 迴圈把單筆函式跑一百次,技術上完全正確。

但效能上,你付出了巨大的代價。

每一次請求都有固定開銷

每一次 API 請求都有固定的開銷:網路來回(network round trip)、建立連線、身份驗證檢查。

這些開銷跟你傳的資料量無關——不管你是查一筆還是查一百筆,每一次請求都得走完這整套流程。

打一百次 API,就是付一百次的固定成本。

同樣的道理也發生在資料庫操作上。

寫入一百筆資料,你可以跑一百次 INSERT,每次塞一筆;也可以跑一次 INSERT,一口氣塞一百筆。

資料量一模一樣,但前者多付了九十九次的連線開銷。

想改成批次,為什麼整條鏈都要動

好,你意識到問題了,想把函式改成一次送一百筆進去。

但你回頭看原本的程式碼:

processUser(id):
  data = callAPI(id)
  result = transform(data)
  return result

每一行都是為「一個」設計的。

先看第一行:processUser(id)。

這一行叫做函式簽名(function signature),就是你在定義一個函式時寫的那一行——函式叫什麼名字、接受幾個參數、每個參數是什麼。

它決定了一件很關鍵的事:後面每一行程式碼會收到什麼東西。

processUser(id) 寫的是「接受一個 ID」,所以下一行的 callAPI(id) 自然就是拿這一個 ID 去送一次請求。

callAPI 回傳的是一筆資料,所以 transform(data) 就是處理這一筆。

transform 處理完是一個結果,所以 return result 就是回傳這一個。

每一行的輸入都來自上一行的輸出,而最源頭就是函式簽名決定的那個「一個 ID」。

現在你想改成一次處理一百筆,第一步就是把函式簽名從「一個 ID」改成「一組 ID」。

但改完之後,callAPI 收到的東西變了,它要改。

callAPI 改了,transform 收到的東西也變了,它也要改。

transform 改了,return 回傳的格式也變了,它也要改。

一路改下去,整條鏈都得重寫。

用陣列取代單筆:批次設計的函式長什麼樣

同樣的需求,換一個方式寫:

processUsers(ids):
  dataList = callAPI(ids)
  results = dataList.map(transform)
  return results

看起來只是改了幾個變數名稱,但每一行的設計邏輯都不一樣了。

processUsers(ids) 接收的是一個陣列——不管裡面有一個 ID 還是一百個 ID,都用同一個入口。

callAPI(ids) 一次把所有 ID 送出去——網路來回只付一次,不是一百次。

dataList.map(transform) 對每一筆回傳資料做處理——不管回來幾筆,這行都能跑。

return results 回傳一個陣列——呼叫端拿到的格式永遠一致。

需要處理一百筆?傳一百個 ID 的陣列進去。

需要處理一筆?傳一個元素的陣列就好:

processUsers([singleId])

同一個函式、同一個流程、同一個回傳格式。

不用開第二個函式,不用寫 if-else 判斷「這次是單筆還是多筆」,不用改任何程式碼。

從 batch 降到單筆是免費的,反過來是重寫

這裡停下來想一下,這兩種方向的成本差多少。

如果你一開始就用 batch(批次,也就是「一次處理一整組」)的方式設計,某天遇到只需要處理單筆的場景,你要做什麼?

傳一個元素的陣列進去,結束。

程式碼不用改,流程不用動,回傳格式也不會變。

成本是零。

但如果你一開始用單筆設計,某天需要處理一百筆,你要做什麼?

前面那段已經演過一次了——函式簽名要改、API 呼叫要改、資料處理要改、回傳格式要改,每一行都要動,整條鏈重寫。

從 batch 降到單筆是免費的。

從單筆升級成 batch 是重寫。

兩個方向的成本完全不對稱。

從函式到系統:處理單位如何影響 API 設計

前面的例子只有一個函式、三行程式碼。

但在真實的系統裡,一個功能通常會經過好幾層:接收請求、打外部 API、處理資料、寫進資料庫、回傳結果給呼叫端。

這些層之間是串在一起的——上一層的輸出,就是下一層的輸入。

單筆設計:「一個」會一路傳下去

當你最外層的函式簽名寫的是「接受一個 ID」,這個「一個」就會一路傳下去。

打 API 那層收到一個 ID,所以送出一次請求。

處理資料那層收到一筆回傳,所以處理一筆。

寫資料庫那層收到一個結果,所以執行一次 INSERT。

回傳給呼叫端的,也是一個物件。

整條鏈從頭到尾,每一層都被鎖在「一個」的軌道上。

等到需求量變大,你會發現不是「某一層」有問題,而是每一層都要跟著改——因為每一層的輸入輸出都是照著「一個」設計的。

批次設計:「一組」也會一路傳下去

反過來,如果最外層的函式簽名寫的是「接受一組 ID」,這個「一組」也會一路傳下去。

API 那層可以一次送出所有 ID,一次請求就搞定。

處理資料那層收到一批回傳,用迴圈跑完。

資料庫那層收到一批結果,一次 INSERT 全部寫進去。

回傳給呼叫端的,是一個陣列。

整條鏈天生就支援批次,不用改任何一層。

業界怎麼做:batch request 與 bulk operation

這種「一次送一整組、一次處理一整批」的做法,在業界有個常見的名字:批次請求(batch request),有時候也叫 bulk operation。

你在很多 API 的設計上都會看到它。

舉幾個例子:資料庫的 bulk insert(一次塞入多筆資料)、HTTP API 的 batch endpoint(一次送出多個請求)、訊息佇列的 batch publish(一次發送多則訊息)。

這些設計背後的邏輯都一樣——把「重複做一件事 N 次」變成「做一次、但處理 N 筆」,省掉 N-1 次的固定開銷。

而這篇文章在講的,就是讓你的程式碼從一開始就具備這個能力。

不是等到系統撐不住了才去串接 batch API,而是在寫第一個函式的時候,就用「一組」當作處理單位,讓整條資料流天生就能對接批次操作。

一個看起來微不足道的設計決定——最外層的函式要接收「一個」還是「一組」——決定了整個系統能不能輕鬆擴展。

資料有層級關係時,batch 還能用嗎?

前面的例子都很扁平:100 個 ID,一次送進去,一次拿回來。

但真實場景的資料通常有層級關係。

層級資料的先後依賴

舉個例子:你要建立一筆訂單,流程是這樣的。

先建立訂單主檔(Order),拿到訂單 ID。

拿著這個訂單 ID,建立 10 筆訂單明細(OrderItem)。

拿著每筆明細的 ID,建立對應的庫存扣減記錄(StockDeduction)。

層與層之間有先後依賴——你沒辦法在還不知道訂單 ID 的時候就建明細,因為明細需要訂單 ID 當作外鍵(foreign key,就是用來關聯兩張表的欄位)。

看到這裡,你可能會想:「這種有層級的資料,是不是就不適合 batch 了?」

第一步:每一層內部還是可以 batch

層與層之間要等 ID,這沒辦法省。

但同一層內部有多筆資料時,還是可以用 batch。

拿到訂單 ID 之後,你要建 10 筆 OrderItem。

這 10 筆之間沒有依賴關係——它們都只需要同一個訂單 ID,彼此不需要等誰先建完。

所以你可以一次 INSERT 10 筆明細,而不是跑 10 次 INSERT 每次塞一筆。

同樣的,拿到 10 筆明細的 ID 之後,你要建 10 筆 StockDeduction,也可以一次 INSERT 全部塞進去。

這就是把前面學到的扁平 batch,套用在每一層裡面。

batch 思維不是「全部打包一次送」,而是「在每一個可以批次的環節都用批次」。

第二步:把整個層級結構打包成一次請求

如果你想更進一步,連層與層之間的來回都可以省。

做法是讓函式一開始就接收整個巢狀結構(nested structure),一次把所有層級的資料都送進去:

createOrder(orderData):
  // orderData 包含:
  //   order 主檔資訊
  //   items: [10 筆明細]
  //   每筆明細底下的 deductions: [扣減記錄]

不是先建 Order 拿 ID、再建 OrderItem、再建 StockDeduction 分三次打。

而是把整棵樹——Order + 底下的 Items + 底下的 Deductions——包成一個巢狀物件,一次送出。

後端收到之後,自己處理 ID 的串接——先建 Order 拿到 ID,再用這個 ID 批次建 10 筆 OrderItem,再用明細的 ID 批次建 StockDeduction。

ID 的產生和傳遞是後端內部的事,呼叫端完全不用管。

第三步:巢狀結構本身也可以是陣列

到這裡,你可能覺得已經很完整了。

但還有一個容易忽略的地方:不要讓巢狀結構又掉回單筆思維。

前面的 createOrder(orderData) 接收的是一棵樹——一筆訂單和它底下的所有明細。

但如果你一次要建 50 筆訂單呢?

如果函式只接收一棵樹,你就得跑 50 次,又回到了 for 迴圈的老路。

所以,函式應該接收的是「一片森林」——一個巢狀結構的陣列:

createOrders(ordersData):
  // ordersData 是一個陣列,裡面有 50 棵樹
  // 每棵樹包含一筆訂單 + 底下的明細 + 底下的扣減記錄

這樣水平和垂直兩個方向都是 batch:

水平方向——一次 50 筆訂單,不是一筆一筆送。

垂直方向——每筆訂單包含完整的層級結構,不是一層一層分開打。

這才是 batch 思維的完整面貌:你的函式從一開始就接收「一組完整的資料結構」,不管資料是扁平的還是有層級的。

拿到需求時怎麼決定處理單位:預設用批次設計

前面講的都是程式碼和系統架構層面的事。

但其實,決定用「一個」還是「一組」來設計,發生在更早的時間點——你剛拿到需求的時候。

需求文件幾乎都用單數描述

需求描述通常是這樣寫的:「使用者上傳一張圖片,系統壓縮後存檔。」

注意這裡的用詞——「一張圖片」、「一個使用者」、「一筆訂單」。

需求文件幾乎永遠用單數來描述,因為它在說的是一個完整的業務流程長什麼樣子。

PM 不會寫「使用者上傳一到一萬張圖片」,那太抽象了。

用單數寫比較好懂,也比較容易溝通。

需求的單位不等於程式碼的單位

但你不需要照著需求描述的單位去設計程式碼。

需求說的是「業務上會發生什麼事」——一個使用者做了一件事,系統怎麼回應。

你要決定的是「技術上用什麼單位來處理」——函式接收一個值,還是一個陣列。

這是兩件不同的事。

需求用單數描述,不代表你的程式碼也要用單數設計。

先問自己:這件事未來會不會變成批次

下次拿到需求的時候,先停下來想一個問題:這件事未來有沒有可能一次處理很多筆?

使用者上傳一張圖?未來可能一次傳十張。

處理一筆訂單?未來可能要跑夜間批次結帳,一次處理當天所有未結訂單。

發一封通知信?未來可能要群發給所有符合條件的使用者,一次發幾千封。

如果答案是「有可能」,甚至是「不確定」,那就預設用一組來設計。

成本很低——只是把參數從一個值變成一個陣列,函式內部用迴圈跑一遍。

但省下的,是未來整條資料流砍掉重練的代價。

前面已經看過那個代價有多大了。

批次設計不適用的情況:同一層內的筆與筆有依賴

前面提到的「層與層之間需要等 ID」不是不適用 batch 的情況——那只是步驟有先後順序,每一層內部還是可以 batch。

真正不適用 batch 的情況是:同一層內,筆與筆之間有依賴。

舉個例子:你有一個計算帳戶餘額的流程,每一筆交易都要讀取上一筆交易算完的餘額,才能算出這一筆的新餘額。

第一筆:餘額 1000,扣 200,新餘額 800。

第二筆:要先知道上一步算出的 800,才能扣 150,得到 650。

第三筆:要先知道上一步算出的 650,才能加 300,得到 950。

這三筆沒辦法打包在一起算,因為每一筆都依賴前一筆的結果。

這才是 batch 無法處理的情況——不是「步驟之間有先後」,而是「同一批資料裡,每一筆都要等上一筆算完」。

但即使遇到這種情況,預設從 batch 介面開始設計,需要時降回單筆(一次只傳一個元素的陣列),永遠比反過來簡單。

你不會因為「預設用陣列」而損失什麼,但你會因為「預設用單筆」在未來付出改寫的代價。

批次設計重點整理

下次寫函式之前,先問自己一個問題:

我的處理單位是什麼?能不能從「一組」開始?

從 batch 降到單筆,傳一個元素就好,零成本。

從單筆改成 batch,整條鏈都要重寫。

這不是什麼進階的架構理論,只是一個簡單的習慣:預設用陣列,讓未來的自己少改一點程式碼。

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

Web API

目錄

  • 單筆處理函式:從一個 ID 到一個結果
  • 用 for 迴圈打 100 次 API 的效能代價
  • 每一次請求都有固定開銷
  • 想改成批次,為什麼整條鏈都要動
  • 用陣列取代單筆:批次設計的函式長什麼樣
  • 從 batch 降到單筆是免費的,反過來是重寫
  • 從函式到系統:處理單位如何影響 API 設計
  • 單筆設計:「一個」會一路傳下去
  • 批次設計:「一組」也會一路傳下去
  • 業界怎麼做:batch request 與 bulk operation
  • 資料有層級關係時,batch 還能用嗎?
  • 層級資料的先後依賴
  • 第一步:每一層內部還是可以 batch
  • 第二步:把整個層級結構打包成一次請求
  • 第三步:巢狀結構本身也可以是陣列
  • 拿到需求時怎麼決定處理單位:預設用批次設計
  • 需求文件幾乎都用單數描述
  • 需求的單位不等於程式碼的單位
  • 先問自己:這件事未來會不會變成批次
  • 批次設計不適用的情況:同一層內的筆與筆有依賴
  • 批次設計重點整理