Open21

Next.js で PlanetScale (+ Prisma) を使う

thesugarthesugar

DB を作る(GUI から DB 名とリージョンを決めるだけで作成できる)
(CLI から作成する場合は pscale database create {DB名}

thesugarthesugar

Prisma のセットアップ

💡 参考 の手順に従っている。

  • Next.js アプリのセットアップ(既存の Next.js プロジェクトに組み込む場合はスキップ)
  • 次に、Next.js プロジェクトのディレクトリで npx prisma init を実行
    • .env と schema.prisma が作成される
thesugarthesugar

.env に DATABASE_URL="mysql://root@127.0.0.1:3309/{さっき決めた DB 名}" を追加

thesugarthesugar

スキーマを定義する

schema.prisma ファイルを開き、以下の内容で更新する:

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
  referentialIntegrity = "prisma"
}

PlanetScale は外部キー制約をサポートしておらず、かつ Prisma はリレーションを表現するのにデフォルトで外部キーを使うようになっているため、上記のように referentialIntegrity プロパティを設定する必要がある。

thesugarthesugar

続いて、このスキーマファイルにデータモデルを定義していく。
ここは(PlanetScale 固有の話ではなく)Prisma の話。

例)

model Inquiry {
id      Int   @default(autoincrement()) @id
name    String
email   String
subject String?
message String
}
thesugarthesugar

Referential integrity について

https://www.prisma.io/docs/concepts/components/prisma-schema/relations/referential-integrity

以下、上記の Prisma のドキュメントより引用(拙訳)。

これを設定している場合、あるレコードが別のレコードを参照しているとき、参照されたレコードは必ず存在している必要がある。
たとえば、Post モデルが author を定義している場合、author も必ず存在していなければならない。

こういった参照を破壊するような変更を加える制約を課すこと、および、レコードの更新時あるいは削除時に実行される referential actions を定義することによって Referential integrity が強制される。
Post モデルの例では、author が削除されたときの処理を定義しておく必要があるということになる。

Prisma では、Referential integrity は @relation 属性を使ってレコード間のリレーションを定義することによって実装(というか実現)される。@relation 属性に対して onUpdate および onDelete 引数を与えることで referential action が被参照レコード(上記例では author)の更新時および削除時に実行される。

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  author   User   @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId Int
}

model User {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

この例では、onDelete: Cascade が指定されており、User が削除されると関連する Post が削除される。
他の Referential actions はこちら

memo

うーん、上で言ってることは理解できるけど結局言われた以上のことはわからない(「つまり外部キー制約が有効な場合のこの操作はこうしないといけないってことか!」みたいな自己解釈を挟んだ上での納得は得られていない。そもそもバックエンドに関してゆるふわ理解駆動開発しかしていないため)

thesugarthesugar

データベースをローカルで実行する

データモデルを定義したら、ターミナルで以下を実行する:
pscale connect {DB名} main --port 3309

このコマンドを実行することで、DB へのローカルのプロキシーが立ち上がる。それによって、ローカルでアプリを動かしているときに DB に接続することが可能になる。

また、上記のコマンドの引数の変更で DB のブランチを変えることができる。

thesugarthesugar

このステップではメインブランチは本番環境へ昇格していない。次のステップで Prisma のスキーマを DB のスキーマと同期させる。
なお、本番ブランチのスキーマを変更することはできない(メモ:直接変更することはできないという意味? 後の部分で詳しい説明が出てくるっぽい)。

thesugarthesugar

新しいターミナルで、以下のコマンドを実行することで prisma.schema で定義したスキーマと PlanetScale のスキーマを同期させる:
npx prisma db push

-> 成功メッセージ "Your database is now in sync with your schema." を確認する。

thesugarthesugar

pscale shell {DB 名} main を実行し、describe {テーブル名};(セミコロン忘れずに)を実行することでテーブルを確認できる(スキーマと DB の同期成功を確認できる)。

⚠️ このとき、mysql-client が必要(マシンに入っていなければ、mysql-client を入れろというメッセージ付きのエラーが出るので、(それに従えばいいため)あまり気にしなくてよい)

thesugarthesugar

pscale branch promote {DB 名} main を実行することで、ブランチを本番に昇格させる。


メモ: ここでの本番 (production) ってどういう意味だろうか(シンプルに考えたら本番環境で使うための DB だと思ってたが)。このあと API を作って DB に書き込んだりするステップが来るが (POST http://localhost:3000/api/hoge)、本番の DB をローカルから更新するということ?(チュートリアルだから?)
チュートリアルを読み進めればわかるかな。

thesugarthesugar

参考にした記事を最後まで読んだけど DB のブランチ運用についてはあまり書かれていなくて、今回は main ブランチだけを作成してそれを開発時も本番時も使うというチュートリアルだから上記のようになったよう。

いずれにしても、本番ブランチが存在しないということはありえないので上記のように main ブランチなどを本番に昇格させて、その後は開発ブランチ (DB) を作成してそれを使って開発→本番ブランチ (DB) にマージという流れ。

ブランチ運用に関しては以下のページがまとまってそう:
https://docs.planetscale.com/concepts/branching

thesugarthesugar

pscale コマンドがうまくいかないときは

ログインできてない可能性あり:pscale auth login でログイン
PlanetScale に複数 Organization 持っている場合は正しい Org が指定されてないかも:pscale org switch {org-name}

thesugarthesugar

ここまでの手順を終えたらあとは Next.js の API Route で以下のように API を定義できる:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();


export default async function handler(req, res) {
    if (req.method === 'POST') {
        return await createInquiry(req, res);
    }
    else {
        return res.status(405).json({ message: 'Method not allowed', success: false });
    }
}

async function createInquiry(req, res) {
    const body = req.body;
    try {
        const newEntry = await prisma.inquiry.create({
            data: {
                name: body.firstName,
                email: body.email,
                subject: body.subject,
                message: body.message
            }
        });
        return res.status(200).json(newEntry, {success: true});
    } catch (error) {
        console.error("Request error", error);
        res.status(500).json({ error: "Error creating question", success:false });
    }
}
thesugarthesugar

実装中に遭遇した事象と対処法のメモなど

thesugarthesugar
  • PrismaClient のインスタンスは以下のように一箇所で new したものを export して使いまわさないと warn(prisma-client)There are already 10 instances of Prisma Client actively running. という警告が出る
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export { prisma }

参考:https://zenn.dev/kanasugi/articles/368d0b39c94daf

thesugarthesugar

Prisma Ver. 4.5.0 以降はスキーマ定義の clientdb は以下のように変更する必要あり。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
  relationMode = "prisma"
}