🔫
SupabaseのDatabase FunctionsとTriggerで、クライアント側から呼び出す実装を削減する
例えば、Supbase x Flutterで開発していてユーザーとルームを作成するボタンにおいて以下のようなFlutterのコードがあったとする。
// テーブル定義
+-----------------+ +-----------------+
| Room | | User |
+-----------------+ +-----------------+
| * id (PK) | | * id (PK) |
| roomKey | | name |
| hostId |------->| roomId (FK) |
| createdAt | | isHost |
+-----------------+ | createdAt |
+-----------------+
// ユーザー(主催者)の作成と同時にルームも作成するボタン
TextButton(
onPressed: () async {
supabase
.from('user')
.insert([
{'name': 'ユーザー名', 'is_host': true, 'room_id': null}
])
.select()
.single()
.then((value) {
final user = User.fromMap(map: value);
supabase // 🔫 ユーザー(主催者)作成に成功したらルームを作成する
.from('room')
.insert([
{'host_id': user.id}
])
.select()
.single()
.then((value) {
final room =
Room.fromMap(map: value, myUserId: user.id);
supabase // 🔫 ルーム作成に成功したらルームIDをユーザー(主催者)に紐付ける
.from('user')
.update({'room_id': room.id})
.eq('id', user.id)
.then((_) {
PushNavigator(context)
.push(route: RoomPage.route());
context.showSuccessSnackBar(
message: 'ルームを作成しました',
);
});
});
})
.catchError((e) {
context.showErrorSnackBar(message: 'ルームの作成に失敗しました');
return null;
});
},
child: const Text(
'作成',
style: TextStyle(fontSize: 16),
),
),
やっていることとしては以下の通り。
-
INSERT
: ユーザー(主催者)を作成 -
INSERT
: ユーザーのidをhost_idになるようにしてルームを作成 -
UPDATE
: ユーザーのroom_idに作成したルームのidをセットして更新する
DBに対する非同期処理を3回やっているので、then
が3回出現している。みたいな感じ。
この実装でもいいが、このような1つの処理を契機に処理をまたぐ場合は、Database Functions と Trigger を使うのが便利。
Database FunctionsとTrigger自体は、従来からあるPostgreSQLの機能。Supabaseはそれをラップして抽象化しているだけである。(なのでこの話自体はPostgreSQL標準の機能を使ったお話)
以下のようなSQLクエリをSupabaseのSQLエディタ上で実行する。
-- ルームを作成するFunction
create or replace function public.handle_create_room() returns trigger as $$
begin
insert into public.room(host_id) values(new.id);
return new;
end;
$$ language plpgsql security definer;
-- ホストユーザー作成時にルームを作成するするTrigger
drop trigger if exists on_new_host_user_created on public.user;
create trigger on_new_host_user_created
after insert on public.user
for each row
when(new.is_host = true)
execute function handle_create_room();
-- ユーザーにroom_idをセットするFunction
create or replace function public.handle_set_host() returns trigger as $$
begin
update public.user set room_id = new.id where id = new.host_id;
return new;
end;
$$ language plpgsql security definer;
-- ルーム作成時にユーザーにroom_idを紐付けるTrigger
drop trigger if exists on_new_room_created on public.room;
create trigger on_new_room_created
after insert on public.room
for each row
execute function handle_set_host();
これを適用すると先ほどFlutter側のコードは以下のように簡潔になる。
TextButton(
onPressed: () async {
supabase
.from('user')
.insert([
{'name': 'ユーザー名', 'is_host': true, 'room_id': null}
])
.select()
.single()
.then((value) {
// この時点でルーム作成とユーザーのroom_idの紐付けは完了している
PushNavigator(context).push(route: RoomPage.route());
context.showSuccessSnackBar(
message: 'ルームを作成しました',
);
})
.catchError((e) {
context.showErrorSnackBar(message: 'ルームの作成に失敗しました');
return null;
});
},
一応補足でFunctionsの方は「Remote Procedure Call」として単体で呼び出せる。が、以下の例の場合、関数内で使用されいているトリガーイベントによって挿入されたnew
レコードが見つからないためエラーになる。
final room_data = await supabase.rpc('handle_create_room');
Discussion