はじめに
Supabaseは手軽に使えるBaaSですが、トランザクション制御をクライアント側から直接扱う機能は公式には提供されていません。
そのため、複数テーブルにまたがる一連の操作を安全に行いたい場合、トランザクションのような処理を自前で工夫して実装する必要があります。
公式の方法ではないのでおそらく推奨でもないでしょうが、知っておくのは何かと良いのではないかと思い記事にしました。
なぜトランザクションが必要?
問題の例
たとえば、以下の2つのテーブル構成があります。
users
はユーザの基本情報(ユーザ名, 自己紹介, GitHubID)を保存するテーブル。
user_skill
は各ユーザに紐づくスキルを登録するテーブル。1ユーザに複数スキルを持たせることができ、user_id
で users
テーブルと関連づけられている。
users
テーブル
カラム名 | 型 | 制約・説明 |
---|---|---|
user_id | uuid | 主キー ユーザID |
name | text | NOT NULL ユーザ名 |
description | text | 自己紹介 |
github_id | text | GitHub ID |
user_skill
テーブル
カラム名 | 型 | 制約・説明 |
---|---|---|
id | number | 主キー |
user_id | uuid | 外部キー(user.user_id) |
skill | text | NOT NULL スキル名 |
たとえば、ユーザー登録時に:
- users テーブルにユーザー基本情報をINSERT
- user_skill テーブルにスキル情報を複数INSERT
このとき、片方だけ成功してもう片方が失敗すると「データの整合性が壊れる」状態になります。
これを避けるために必要なのが 「すべて成功か、すべて失敗か」 を保証する トランザクション です。
Supabaseではどうするか(→rpcを使ってストアドプロシージャを呼び出す)
Supabaseでは、クライアントからトランザクションを制御するAPIはありません。
そのため、Supabase側でPostgreSQLの関数(=ストアドプロシージャ)を定義し、クライアントから rpc()
を使って呼び出すことで、無理やりトランザクションライクな操作が実現できます。
方法
ストアドプロシージャ定義(Supabase側)
- 該当のプロジェクト > SQL Editorにて、ストアドプロシージャを作成(作成例は以下)
- 右下の「Run」を実行
- 左メニュー > Database > Functionにて、作成できているか確認できます
PostgreSQL関数の定義
create or replace function insert_user_and_userskill(
_name text,
_description text,
_github_id text,
_skills text[]
)
returns void
language plpgsql
as $$
declare
new_user_id uuid;
begin
-- ユーザー情報を users テーブルに挿入し、生成された user_id を取得
insert into users (name, description, github_id)
values (_name, _description, _github_id)
returning user_id into new_user_id;
-- スキル配列を展開して、user_skill テーブルに一括挿入
insert into user_skill (user_id, skill)
select new_user_id, skill from unnest(_skills) as skill;
-- 正常終了:何も返さない
return;
exception
-- 途中で何らかのエラーが発生した場合、エラーメッセージ付きで例外を投げる
when others then
raise exception '登録処理でエラー発生: %', SQLERRM;
end;
$$;
PostgreSQLの関数は1関数=1トランザクション単位で処理されるため、明示的なbeginやcommitは不要。
unnest
は配列を行に展開するSQL関数
クライアント側の呼び出し例(TypeScript)
const { error } = await supabase.rpc('insert_user_and_userskill', {
_name: '田中一郎',
_description: 'フロントエンドエンジニア',
_github_id: 'tanaka123',
_skills: ['React', 'TypeScript'],
});
if (error) {
// → PostgreSQLのraiseで指定したエラーメッセージがここに出る
console.error('登録エラー:', error.message);
} else {
// → 正常終了時は error が null で、data も null
console.log('登録成功!');
}
Supabaseクライアントの設定(Url, AnonKey)は省略
最後に
Supabaseでは、現時点では公式にトランザクションAPIがあるわけではありません。
しかしPostgreSQLのストアドプロシージャ(関数)を活用し、rpc() 経由で実行することで、Supabaseでもトランザクションっぽいことができました。
その他懸念点
- テストやモックでの扱いがやや面倒…?
-
raise
による例外は文字列しか返せないらしい。エラーハンドリングの際は、クライアントでの細かい設計が必要 - rpc関数はRLSをバイパスすることがあるらしい。認可周りに要注意
-
drizzle ORM
を使った方法もあるようです。全く知らないので、引き続き調査
参考
https://github.com/orgs/supabase/discussions/4562#discussioncomment-1844073
https://zenn.dev/smallstall/articles/596d3981984587