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 全都整合進同一個物件。下一篇見。