RPC 呼叫資料庫函式 | Supabase 完整教學

2026/10/04
RPC 呼叫資料庫函式 | Supabase 完整教學

學會了《Embedded Resources 關聯查詢》之後,你已經能用一支 API 把整棵關聯資料樹撈回來。但有些需求,光靠自動 API 的宣告式查詢還是不夠——多步驟交易(如轉帳要嘛全成功、要嘛全失敗)、跨多表的複雜聚合、需要繞過或精細控制權限的管理操作。這些更適合封裝進 PostgreSQL 的資料庫函式,再用 Supabase 的 RPC(supabase.rpc('fn', { args }))一支呼叫觸發。這一篇帶你把 RPC 用到位:對應 Postgres function、依名稱傳參數、回傳 scalar/setof/table、對 SETOF 結果串接 PostgREST 篩選、security definer 搭配 search_path,並看懂「RPC 其實也走 REST」(/rest/v1/rpc/fn)的原理,以及 pg_graphql 概觀。

前言

一句話定義本篇主題:RPC(Remote Procedure Call,遠端程序呼叫)是 Supabase 讓你「用一支 API 呼叫你事先在 PostgreSQL 裡寫好的資料庫函式」的能力——你把複雜的業務邏輯(多步驟交易、聚合計算、提權操作)寫成一支 Postgres function,前端只要 supabase.rpc('函式名', { 參數 }),PostgREST 就會把它翻譯成對 /rest/v1/rpc/函式名 的一次 HTTP 請求、在資料庫端執行整段邏輯,再把結果回傳。

上一篇《Embedded Resources 關聯查詢》我們把關聯查詢用到位——靠外鍵把整棵資料樹一次 join 回來。到目前為止,我們用的都是 PostgREST 的宣告式查詢:.from().select().eq().order(),你「描述你要什麼資料」,資料庫幫你算出來。這套機制對「讀取、篩選、關聯展開」非常好用,但它有個天生的邊界:它不擅長表達「程序性邏輯」——「先做 A,如果成功再做 B,否則整個回滾」這種帶有步驟、條件、交易的流程,沒辦法塞進一個 select 字串裡。RPC 正是為此而生。

用一個類比:如果說自動 API 是「自助餐夾菜」,那 RPC 就是「跟廚房點一道招牌菜」。 自助餐(.select())你自己走過去,看得到的菜色自己夾、自己搭配,靈活但只能拿現成的。而「佛跳牆」這種要燉好幾個鐘頭、工序繁複、還牽涉火候與食材搭配的功夫菜,你不會想自己在餐檯前現做——你直接跟廚房(資料庫)點:「來一份佛跳牆」(.rpc('make_foguo')),廚房照著食譜(你事先寫好的 function)把整套工序在後場做完,端一盤成品給你。食譜寫在廚房裡(邏輯在資料庫端),你只要報菜名和幾個客製參數,其餘的複雜步驟都不用你操心,而且整套工序是一氣呵成的(交易的原子性)。

本篇你會學到:

  • RPC → function → REST 的對應:supabase.rpc('fn', { args }) 到底發生了什麼、為什麼說它「也走 REST」
  • 傳參數與回傳型別:參數依名稱對應、回傳 scalar/setof/table 三種形態,以及對 SETOF 結果串接 PostgREST 篩選
  • 封裝交易邏輯:用 PL/pgSQL 把「轉帳」這種多步驟操作寫成一支原子函式
  • security definer 搭配:什麼時候需要提權、以及一定要加的 search_path 防護
  • pg_graphql 概觀:除了 REST,Supabase 也提供 GraphQL 端點

核心概念

RPC 的心智模型,可以濃縮成一條對應鏈。先把它刻進腦中:

你在 PostgreSQL 寫一支 function(例如 add_numbers(a, b))
        │
        ▼
PostgREST 讀系統目錄,把它映射成一個端點 /rest/v1/rpc/add_numbers
        │
        ▼
你在前端呼叫 supabase.rpc('add_numbers', { a: 10, b: 20 })
        │
        ▼
supabase-js 送出 POST /rest/v1/rpc/add_numbers  body: { "a": 10, "b": 20 }
        │
        ▼
PostgREST 執行 SELECT add_numbers(a := 10, b := 20),把回傳結果當 JSON 送回

這條鏈裡有三個關鍵觀念要說清楚。

一、RPC 也走 REST——它不是另一套協定

很多人以為 RPC 是「跟 REST 平行的另一種 API」,其實不然。在 Supabase/PostgREST 的世界裡,RPC 只是 REST API 的一條特殊路徑:你的每一支資料庫函式,都會被自動映射成 /rest/v1/rpc/<函式名> 這個端點。呼叫它就是對這個 URL 發一次 HTTP 請求——有參數時用 POST(參數放在 JSON body),唯讀函式可以用 GET(參數放 query string,可被快取)。supabase.rpc() 只是幫你把這個請求組好送出的語法糖。所以你在瀏覽器的 Network 面板看 .rpc(),看到的就是一支再普通不過的 POST /rest/v1/rpc/...。

二、參數是「依名稱」對應的

這是 RPC 最容易踩雷、也最重要的一點:PostgREST 是用「參數名稱」而非「位置」來對應函式引數的。 你 SQL 函式簽章寫 add_numbers(a int, b int),那 .rpc() 第二個引數物件的鍵名就必須剛好是 a 和 b:

// 對:鍵名 a、b 與函式簽章的參數名一致
supabase.rpc('add_numbers', { a: 10, b: 20 })

// 錯:鍵名對不上 → PostgREST 找不到「簽章相符」的函式,報 PGRST202
supabase.rpc('add_numbers', { x: 10, y: 20 })

底層 PostgREST 執行的是類似 SELECT add_numbers(a := 10, b := 20) 的具名參數呼叫。記住這一點,能省下你 debug 一大半的 RPC 錯誤。

三、回傳型別決定結果的形態

函式的 RETURNS 宣告,決定了 .rpc() 拿到的 data 長什麼樣:

函式回傳宣告data 形態範例用途
returns int / text / json(scalar)單一純量值或 JSON 物件加總、狀態旗標、json_build_object 結果
returns setof <table>資料列陣列(可再串接篩選)「依分類取商品」這類回傳多筆的查詢
returns table(...)資料列陣列(自訂欄位結構)回傳自訂欄位的報表

關鍵術語一次看懂:

  • RPC(Remote Procedure Call):透過 API 呼叫遠端(資料庫端)的一段程序。
  • Database Function(資料庫函式):用 create function 定義、跑在 PostgreSQL 裡的函式,可用 sql 或 plpgsql 語言撰寫(呼應第 007 篇《Database Functions 與 Triggers》)。
  • SETOF / RETURNS TABLE:函式回傳「多列」的兩種寫法,.rpc() 拿到陣列。
  • security definer / security invoker:函式以「定義者」還是「呼叫者」的權限執行,決定會不會受 RLS 限制。

實作範例

以下範例用一組簡單的資料表:products(商品,有 category 與 price)與 accounts(帳戶,有 user_id 與 credits)。所有 SQL 都可以直接貼進 Supabase Dashboard 的 SQL Editor 執行。

先假設已建好 client:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

範例一:最簡 scalar 函式——回傳單一值

從最單純的加法函式開始,感受「寫函式 → 呼叫」的完整流程。這支函式回傳一個 int(scalar):

-- 純計算函式:接兩個整數、回傳它們的和
create or replace function add_numbers(a int, b int)
returns int
language sql
as $$
  select a + b;
$$;

前端呼叫——注意參數鍵名 a、b 必須與函式簽章一致:

// scalar 回傳 → data 直接是那個值
const { data, error } = await supabase.rpc('add_numbers', { a: 10, b: 20 })

console.log(data) // 30

對照的 REST 請求——這就是 .rpc() 底層送出的東西。看清楚它就是一支對 /rest/v1/rpc/ 的 POST:

curl -X POST "$SUPABASE_URL/rest/v1/rpc/add_numbers" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"a": 10, "b": 20}'

# 回傳:30

.rpc('add_numbers', { a: 10, b: 20 }) 就是 POST /rest/v1/rpc/add_numbers、body 為 {"a":10,"b":20}。RPC 也走 REST,這下具體了吧。

範例二:回傳 SETOF table——並串接 PostgREST 篩選

RPC 不只能回傳單一值,還能回傳一整批資料列。當函式宣告 returns setof <table> 時,.rpc() 拿到的是陣列,而且最妙的是——你可以像對一般表查詢那樣,在後面繼續串接 PostgREST 的篩選、排序、分頁:

-- 依分類取商品:回傳 setof products
create or replace function get_products_by_category(category_name text)
returns setof products
language sql
as $$
  select * from products where category = category_name;
$$;

前端呼叫並串接篩選——這是 RPC 一個非常實用的特性:

// 回傳 setof → 可再串接 .lt() / .order() / .limit()
const { data } = await supabase
  .rpc('get_products_by_category', { category_name: 'electronics' })
  .lt('price', 1000)                     // 對 RPC 結果再篩選:價格 < 1000
  .order('price', { ascending: true })   // 排序
  .limit(20)                             // 取前 20 筆

// data 例如:
// [ { id: 1, name: '滑鼠', category: 'electronics', price: 599 }, ... ]

這裡發生的事是:函式先在資料庫端把 electronics 分類的商品選出來,PostgREST 再把 .lt('price', 1000)、.order()、.limit() 套在這個結果集之上,一併下推到 SQL 執行。所以你既享有「函式封裝的邏輯」,又保有「在呼叫端彈性下條件」的靈活度。

若這支函式是唯讀的(不修改資料),還能用 GET 呼叫、讓結果可被 CDN/瀏覽器快取:

// { get: true } 標記為唯讀,改用 GET 請求(參數走 query string,可快取)
const { data } = await supabase.rpc(
  'get_products_by_category',
  { category_name: 'electronics' },
  { get: true }
)

範例三:封裝交易邏輯——轉帳(多步驟原子操作)

這是 RPC 最能發揮價值的場景:多步驟交易。想像「A 轉點數給 B」:要先從 A 扣款、再給 B 加款,中間任何一步失敗都必須整個回滾——絕不能出現「A 扣了、B 沒加」的錯帳。這種原子性,只有把整段邏輯寫進資料庫函式、包在同一個交易裡才保證得了。用 PL/pgSQL 撰寫:

-- 轉帳:多步驟、帶條件檢查、原子交易
create or replace function transfer_credits(
  from_user_id uuid,
  to_user_id uuid,
  amount int
)
returns json
language plpgsql
security definer          -- 以定義者權限執行(見下段說明)
set search_path = public  -- security definer 必加的防護
as $$
declare
  result json;
begin
  -- 步驟一:從來源帳戶扣款(且餘額必須足夠)
  update accounts
    set credits = credits - amount
    where user_id = from_user_id and credits >= amount;

  -- 若沒有任何列被更新,代表餘額不足 → 拋例外,整個交易自動回滾
  if not found then
    raise exception '餘額不足(Insufficient credits)';
  end if;

  -- 步驟二:給目標帳戶加款
  update accounts
    set credits = credits + amount
    where user_id = to_user_id;

  -- 組裝回傳的 JSON
  select json_build_object('success', true, 'transferred', amount) into result;
  return result;
end;
$$;

函式體內的兩個 update 天然跑在同一個交易裡:只要 raise exception(例如餘額不足),PostgreSQL 就會把這次呼叫的所有變更全部撤銷。前端呼叫起來,卻只是輕鬆的一行:

// 一支 RPC 觸發整段轉帳交易
const { data, error } = await supabase.rpc('transfer_credits', {
  from_user_id: 'a1b2...',
  to_user_id: 'c3d4...',
  amount: 100
})

if (error) {
  // 餘額不足等錯誤,會以 error 回傳
  console.error(error.message)  // 例如:餘額不足(Insufficient credits)
} else {
  console.log(data) // { success: true, transferred: 100 }
}

試想:如果不用 RPC,你得在前端發兩支 update——先扣 A、再加 B。萬一第一支成功、第二支因網路或當機失敗,錢就憑空蒸發了,而且前端根本無法保證兩步的原子性。交易邏輯必須留在資料庫端,這就是 RPC 存在最硬的理由。

範例四:陣列參數與 RETURNS TABLE

參數也可以是陣列,回傳也可以用 returns table(...) 自訂欄位結構:

-- 陣列參數 + 自訂回傳欄位
create or replace function order_summary(product_ids int[])
returns table(product_id int, name text, price numeric)
language sql
as $$
  select id, name, price
  from products
  where id = any(product_ids);
$$;
// 陣列直接當參數值傳入
const { data } = await supabase.rpc('order_summary', {
  product_ids: [1, 3, 7]
})

// data 例如:
// [ { product_id: 1, name: '滑鼠', price: 599 }, ... ]

關於 security definer 與 search_path

範例三用了 security definer,這裡把它講透。函式有兩種執行權限模式:

模式以誰的權限執行是否受 RLS 限制使用場景
security invoker(預設)呼叫者是,受呼叫者 RLS一般業務邏輯
security definer定義者(通常是 postgres)否,繞過 RLS需提權的跨表/管理操作

轉帳為什麼需要 security definer?因為 accounts 表通常有 RLS,只允許使用者讀寫自己那一列;但轉帳必須同時改動「來源」與「目標」兩個人的帳戶,一般使用者權限做不到。用 security definer 讓函式以更高權限執行,就能安全地完成跨列操作,而不必把 service_role 金鑰暴露到前端。

但提權有風險,所以有一條鐵律:security definer 函式一定要加 set search_path = public(或明確指定 schema)。否則惡意使用者可以竄改自己 session 的 search_path,誘使函式去呼叫他預先埋好的同名惡意物件,形成提權攻擊。加上固定的 search_path,函式就只會解析到你預期的 schema 內的物件。此外,security definer 函式內最好自己再做一層權限檢查(例如驗證 auth.uid() 是否有資格),別把提權能力無條件開放給任何呼叫者。

補充:pg_graphql 概觀

除了 REST(含 RPC),Supabase 還透過官方維護的 pg_graphql extension 額外提供一個 GraphQL 端點 /graphql/v1,同樣自動從你的 SQL schema 生成。啟用只要一行:

create extension if not exists pg_graphql;

之後每張有主鍵的表都會映射成一個 GraphQL type 與 xxxCollection 查詢,支援 cursor-based 分頁與 filter/orderBy:

query {
  productsCollection(first: 10, filter: { price: { lt: "500" } }) {
    edges { node { id name price } }
    pageInfo { hasNextPage endCursor }
  }
}

要不要用 GraphQL 看團隊偏好——本系列的存取層主要聚焦在 REST + RPC,pg_graphql 在此僅作概觀,知道「Supabase 也有 GraphQL 這條路」即可。

常見錯誤與最佳實踐

坑一:參數名稱不符,報 PGRST202 找不到函式。 PostgREST 用「參數名稱」對應函式引數。函式簽章是 add_numbers(a, b),你卻傳 { x, y },PostgREST 就找不到簽章相符的函式。正確做法:.rpc() 物件的鍵名必須與 SQL 參數名逐字一致(含大小寫)。debug RPC 時,第一個要檢查的永遠是「參數名對不對得上」。

坑二:RPC 沒設好權限,前端呼叫被拒。 函式雖然建好了,但若 anon / authenticated 角色沒有 EXECUTE 權限(例如你手動 REVOKE 過),前端呼叫會失敗。正確做法:確保目標角色有執行權限,GRANT EXECUTE ON FUNCTION fn_name TO anon, authenticated;。Supabase 對 public schema 的函式預設會授權,但自訂 schema 或手動撤權後要記得補回。

坑三:把「該用查詢的」都硬塞進 RPC。 單純的讀取、篩選、排序、關聯展開,PostgREST 的自動 API(.from().select())已經又快又好,包成 RPC 只是徒增維護成本、還失去在 URL 上彈性下條件的能力。正確做法:宣告式查得出來的用 select;只有「多步驟交易、複雜聚合、需要提權」才用 RPC。RPC 是為「程序性邏輯」而生,不是查詢的替代品。

坑四:security definer 忘了設 search_path。 提權函式沒鎖 search_path,等於開了一道 schema 劫持的後門。正確做法:每一支 security definer 函式都加 set search_path = public(或明確 schema),並在函式內自己做權限與輸入檢查。

坑五:新建/改了函式,卻立刻呼叫失敗。 PostgREST 有 schema cache,新建或改動函式簽章後需要數秒才會重載。正確做法:稍等幾秒,或到 Dashboard → API → Reload Schema 手動重載後再試。

坑六:以為 SETOF 回傳不能再篩選。 不少人拿到 RPC 結果就在前端自己 .filter(),白白傳輸多餘資料。正確做法:只要函式 returns setof <table>,就能直接在 .rpc() 後串接 .eq()/.lt()/.order()/.limit(),讓篩選下推到資料庫,省頻寬也更快。

最佳實踐總結:

  • 參數逐字對名:.rpc() 鍵名 == SQL 參數名(含大小寫)
  • RPC 只裝該裝的:交易、複雜聚合、提權才用 RPC;一般查詢用 select
  • 交易邏輯留在資料庫:多步驟原子操作用 PL/pgSQL + raise exception 保證回滾
  • security definer 必配 search_path:並在函式內自做權限檢查
  • 善用 SETOF 串接:回傳表格型結果就在呼叫端下條件,別在前端 filter
  • 記得授權與重載:GRANT EXECUTE,改完函式等 schema cache 重載

小結

這是 Supabase 系列教學的第 015 篇,也是第二大主題 SB-2「存取層」的收尾。上一篇《Embedded Resources 關聯查詢》帶你靠外鍵把整棵關聯資料樹一次撈回;這一篇則跨出「宣告式查詢」的邊界,進入程序性邏輯的世界——用 RPC 呼叫你在 PostgreSQL 裡寫好的資料庫函式,把交易、聚合與提權操作收進資料庫、用一支 API 觸發。

核心觀念濃縮成一句話:RPC 是 REST API 的一條特殊路徑(/rest/v1/rpc/fn),supabase.rpc('fn', { args }) 依「參數名稱」把引數傳給你事先寫好的 Postgres function,回傳可以是 scalar、setof 或 table——setof 結果還能串接 PostgREST 篩選;多步驟交易寫成 PL/pgSQL 保證原子性,需要提權時搭配 security definer 並務必鎖定 search_path。

  • RPC 也走 REST:.rpc() 底層就是 POST /rest/v1/rpc/fn,唯讀函式可用 GET 快取
  • 參數依名稱對應:鍵名對不上是最常見的錯,逐字一致才找得到函式
  • 回傳型別決定形態:scalar 回單值、setof/table 回陣列且可串接篩選
  • 交易與提權是 RPC 的主場:多步驟原子操作、跨表管理,搭配 security definer + search_path
  • 別濫用:宣告式查得出來的用 select,RPC 留給真正的程序性邏輯

這支 RPC 也呼應了本系列第 007 篇《Database Functions 與 Triggers》——當時我們在資料庫端寫好了函式,現在你學會了如何從應用端優雅地呼叫它。至此,SB-2 存取層(查詢、篩選、分頁、關聯查詢、RPC)已經完整。你已經能用 REST API 對 Supabase 資料庫進行各種讀寫與邏輯呼叫了——但一直以來我們都直接用 curl 或裸的 .from()/.rpc(),還沒好好認識那個貫穿全系列的主角:supabase-js 客戶端。下一篇《supabase-js 客戶端入門》,我們就來系統性地認識這個 SDK:如何建立 client、管理環境變數與金鑰、以及它如何把 Auth、Realtime、Storage 全都整合進同一個物件。下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →