PostgreSQL トリガー関数を Prisma + Supabase でうまく使う方法

ある開発案件における、prismaでトリガー関数をうまくGitバージョン管理に含めたかったので、実践したその方法の紹介です。

- Twitter やってますのでこちらもご覧ください →[Twitter @takumafujimoto](https://twitter.com/takumafujimoto)
- プログラミングコースも出したりしてます → [コース一覧](https://www.takux.one/)

## トリガー関数をマイグレーション履歴に含めて開発が👍

Supabaseではダッシュボード上で、PostgreSQLのファンクションやトリガーを設定できます。

便利ですが、Git/Githubなどで開発を進めている場合、何もしないとソースコードに載ってこないのは不便です。

例えば開発用と本番用でデータベースを分けている場合、両方とも手動で設定しないといけないですし、

開発用などで頻繁にデータベースをリセットする場合は特に大変です。

Prismaは便利なORMサービスです。

マイグレーションもしっかりできるので、データベーススキーマ（モデル）定義は変更の都度、SQLファイルとして残っていきます。

プロジェクトでPrismaを使用している場合はこのマイグレーション履歴にトリガー関数のSQL文を含めることで、開発をスムーズに進めることが可能です。

## 前提

- 今回はNext.jsのプロジェクトで使ってみたいと思いますが、Next.jsに限らず応用できる内容です。
- DBについても今回はSupabaseですが、他でも問題ないと思います。
- Supabaseの初期設定などについては解説しませんので、ググってみてください。

## サンプルプロジェクトの準備

Next.jsはサンプルなので Create Next App で作成したいと思います。

```
npx create-next-app@latest --ts
```

prismaをインストールします。

```
yarn add -D prisma
yarn add @prisma/client
```

prismaを初期化します。

```
npx prisma init
```

プロジェクトに prisma ディレクトリが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/9f057b78-24e5-d456-40b4-9383f491133c.png)


supabaseとの接続は詳細は省きますが、.envや.env.localなどのファイルに下記のように記載します。

それぞれの項目はご自身のsupabaseの情報に置き換えてください。

```:.env
DATABASE_URL=xxx
SUPABASE_URL=xxx
SUPABASE_KEY=xxx
```

## Prismaスキーマ定義

prismaディレクトリの中のshema.prismaファイルにモデルを定義します。

今回はシンプルに下記のような例で定義します。

```jsx:prisma/shema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model post {
  id    Int    @id @default(autoincrement())
  title String
}

model notification {
  id    Int    @id @default(autoincrement())
  title String
}
```

postテーブルは投稿を、 notificationテーブルはトリガー関数が実行された時に挿入される役目とします。

## はじめのマイグレーション

それではまずはここで初めのマイグレーションを行なってみます。

```
npx prisma migrate dev
```

マイグレーション履歴の名前（フォルダ名）を求められますが、ここでは `first_migration` と入力しておきます。入力せずにエンターでも大丈夫です。

完了すると、次のように migrations フォルダや sql文が記載されたファイル（migration.sql）などが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/ef131c08-c79a-9a93-8699-ced2b0c5dae9.png)


PrismaStudioを立ち上げてテーブルを確認してみます。

```
npx prisma studio
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/df59d4ed-a4ef-df8e-028c-f16b86a22404.png)


テーブルができています。

一応supabaseダッシュボードからも確認すると次のように作成されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/488b1225-5f51-a880-ffb5-75f055a14cc3.png)


## サンプルとして投稿機能を実装

簡単に投稿機能をつけてみます。

pages/api 直下に posts.ts ファイルを作成し、postを作成するエンドポイントとします。


```tsx:pages/api/posts.ts
import { PrismaClient } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";

const prisma = new PrismaClient();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const post = await prisma.post.create({
      data: {
        title: req.body.postTitle,
      },
    });
    if (!post) throw Error("not created");
    res.status(200).json(post);
  } else {
    res.setHeader("Allow", "POST");
    res.status(405).end("Method Not Allowed");
  }
}
```

次にpages直下のindex.tsxファイルを次のようにします。



```tsx:pages/index.tsx
import type { NextPage } from "next";

const Home: NextPage = () => {
  const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault();
    const response = await fetch("/api/posts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        postTitle: e.currentTarget.post_title.value,
      }),
    });
    const createdPost = await response.json();
    console.log(createdPost);
  };
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input type="text" name="post_title" placeholder="post title" />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export default Home;
```

この状態で `yarn dev` でサーバーを立ち上げて確認してみます。

見にくいですがフォームが設置できています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/61cc0edd-07f2-b6e5-93b7-de146dbd4f30.png)


試しに `TITLE 1` と入力して送信してみます。

うまくいけばコンソールに作成された post が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/d863e650-5699-815d-96d3-1c6a9f4b4dbf.png)


Prisma Studio でも確認しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/c4c46723-2e68-cea5-4b54-40874ec7d2be.png)


## トリガー関数をマイグレーション履歴に含める

ここから本題のトリガー関数の作成です。

今回はサンプルとして、postテーブルに新しくレコードが挿入（INSERT）されたら、それをトリガーとして notificationテーブルにもレコードを挿入するようにします。

トリガー関数は shema.prisma に記載するわけではないので、sqlファイルを用意する必要があります。

prismaでは次のようにするとファイルのみ生成可能です。

```
npx prisma migrate dev --create-only
```

またまた名前を聞かれるので仮に `trigger_function` と入力しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/87e16ca4-e6a8-b392-a7b1-212f7be9f62a.png)


xxx_trigger_function フォルダの中の migration.sql ファイルを編集します。

```sql:prisma/migrations/xxx_trigger_function/migration.sql
create or replace function public.insert_notification()
  returns trigger as $$
begin
  insert into public.notification(title) 
  values (new.title || ' was created');
  return new;
end;
$$ language plpgsql;

create trigger on_insert_post
  after insert
  on public.post
  for each row
  execute procedure public.insert_notification();
```

- 関数名: insert_notification
- トリガー名: on_insert_post

上記は post テーブルに新しくレコードが挿入された場合に発動し、

notificationテーブルにもレコードを生成します。

このとき、postレコードの title フィールドの値を使用して、

`[その挿入されるレコードのタイトル] + " was created"` という文字列とし、

notificationレコードのタイトルとします。

それではこれを適用するためにマイグレートを行います。

```
npx prisma migrate dev
```

supabaseを確認すると、Database > Functions のところに `insert_notification` という関数が作成されています。トリガーについても同様に作成されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/b1905f87-aa87-a409-8b45-3d274513b99d.png)


では想定通り機能するか、試してみます。

まずはトップページで今度は `TITLE 2` と入力して送信してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/0c1806aa-2d5b-8a2f-2cfa-004c608fe6e4.png)


これは通常通り postに追加されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/1e267dae-f49d-e3dd-63d4-ea6b410ba7b4.png)


合わせてトリガー関数がうまくいっているか、notificationテーブルを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706663/7bccb3ab-473f-ba0d-11cc-16ce840696f7.png)


notificationにもしっかりとデータが登録されていることが確認できました。

PrismaとSupabaseでトリガー関数を使う方法については以上です！