🐹

Supabaseの関数をNext.jsアプリから呼び出してトランザクション処理をする

2022/03/07に公開

前回の記事でSupabaseを使って、簡単な関数を作成し、Next.jsのアプリから呼びだしてみました。

https://zenn.dev/hrtk/articles/supabase-nextjs-database-function

今回は、テーブルの操作を絡めた関数を作成してみます。

本記事は、Supbaseが公開している動画のコードをベースに、ステップバイステップで自身の環境で試せるようにアレンジしています。

https://www.youtube.com/watch?v=I6nnp9AINJk

セットアップ

前回の記事をベースにしていきます。

degit経由で取得して、必要なパッケージをインストールします。

npx degit hirotaka/examples/supabase-nextjs-database#atlanta-0.0.4 supabase-nextjs-database
cd supabase-nextjs-database
npm install

環境変数のセットなどは次を参考にしくてださい。

https://zenn.dev/hrtk/articles/supabase-nextjs-database-authentication#next.jsアプリをセットアップ

コンポーネントを作成

まずは、コンポーネントを作成して、クリックした数を表示するようにしてみましょう。

components/counter.tsx
import { useState } from 'react'

export default function Counter() {
  const [clicked, setClicked] = useState(0)
  const [loading, setLoading] = useState()

  const incrementCount = async () => {
    setClicked(clicked + 1)
  }

  return (
    <div className="mt-6">
      <label className="block">カウンター</label>
      <p className="mt-2">{loading ? '読み込み中...' : `${clicked}`}</p>
      <button className="primary mt-4" onClick={incrementCount}>
        クリック
      </button>
    </div>
  )
}

ダッシュボードにコンポーネントを読み込みます。

components/dashboard.tsx
@@ -1,15 +1,17 @@
 import { useUser } from '@/contexts/user'
 import Message from '@/components/message'
 import Profile from '@/components/profile'
+import Counter from '@/components/counter'
 
 export default function Dashboard() {
-  const { logout } = useUser()
+  const { logout, user } = useUser()
 
   return (
     <div className="max-w-md mx-auto">
       <h1 className="text-2xl">ダッシュボード</h1>
       <Message />
       <Profile />
+      {user.profile && <Counter />}
       <div className="mt-6">
         <button className="secondary" onClick={() => logout()}>
           ログアウト

これでクリックボタンをクリックすると、カウントアップすることを確認できます。

このままだと、再読み込みをしてしまうと0に戻ってしまいます。

関数を追加

Supbaseのテーブルにクリック数を保持して、インクリメントする関数を作成します。

SQL Editorでprofilesテーブルにクリック数を保持するカラムを追加するSQLを実行します。

alter table profiles add column clicks int8 default 0;

まず、アプリでprofilesテーブルのclickesカラムの値を取得して表示します。

components/counter.tsx
@@ -1,8 +1,15 @@
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
+import { useUser } from '@/contexts/user'
+import { supabase } from '@/lib/supabase-client'
 
 export default function Counter() {
   const [clicked, setClicked] = useState(0)
   const [loading, setLoading] = useState()
+  const { user } = useUser()
+
+  useEffect(() => {
+    if (user) setClicked(user.profile.clicks)
+  }, [user])
 
   const incrementCount = async () => {
     setClicked(clicked + 1)

例えば、Table Editorでprofilesテーブルのclicksの値を「12」にしてみます。

再読み込みすると、クリック数にTable Editorで入力した値が表示されることを確認できます。

次に、クリック数をインクリメントする関数を作成します。

下記のコードをSQL Editorで実行します。

create or replace function increment_clicks()
returns int8
language plpgsql
as $$
declare
  new_clicks int;
begin
  -- 現在のクリック数をselect
  select clicks
  from public.profiles
  where id = auth.uid();
  -- クリック数をインクリメント
  new_clicks = new_clicks + 1;
  -- クリック数を更新
  update public.prifiles
  set clicks = new_clicks
  where id = auth.uid();
  -- インクリメントした数を返す
  return new_clicks;
end;
$$;

関数内で、2つのSQLを実行しています。これによりアプリからは1回の呼びだしだけで1連の処理を実現します。

アプリのincrementCountメソッドを書き換えて、Supabaseの関数を呼びだすようにします。

components/counter.tsx
@@ -12,15 +12,25 @@ export default function Counter() {
   }, [user])
 
   const incrementCount = async () => {
-    setClicked(clicked + 1)
+    try {
+      setLoading(true)
+      const { data } = await supabase.rpc('increment_clicks')
+      setClicked(data)
+    } finally {
+      setLoading(false)
+    }
   }
 
   return (
     <div className="mt-6">
       <label className="block">カウンター</label>
       <p className="mt-2">{loading ? '読み込み中...' : `${clicked}`}</p>
-      <button className="primary mt-4" onClick={incrementCount}>
-        クリック
+      <button
+        className="primary mt-4"
+        onClick={incrementCount}
+        disabled={loading}
+      >
+        {loading ? '読み込み中...' : 'クリック'}
       </button>
     </div>
   )

関数のreturn句で指定した値がrpcdataとして返されます。その値をステートに保持します。

ボタンをクリックすると、Supbaseの関数が呼びだされ、クリック数をインクリメントして保存されます。

これで、再読み込みしても、保存されたクリック数が表示されるのを確認できます。

ここまでの、コード全体はGitHubリポジトリーを参照してください。

https://github.com/hirotaka/examples/tree/atlanta-0.0.5/supabase-nextjs-database

おわりに

SupbaseでテーブルをSELECTして、UPDATEする簡単なトランザクション処理する一連の流れを紹介しました。関数を使うことで、制御構造や複雑な演算をPostgresで処理ができます。

もちろん、同様な処理をミドルウェアで処理も可能です。ただ、その場合に比べて、複数の問い合わせをひとまとめに処理し、ネットワークのオーバーヘッドを軽減できます。

ただ、「PL/pgSQLでの記述は…」という方もいらっしゃることでしょう。その場合は、plv8という拡張機能を使うことで、JavaScriptでの記述ができます。

次回以降でまた詳しく紹介して参ります。

参考

Supbaseの知識を深めるために、ドキュメントの翻訳に取り組んでいます。

https://supabase.jp/

関数について、こちらも参考にしてください。

https://supabase.jp/docs/guides/database/functions

GitHubで編集を提案

Discussion