Next.js で PlanetScale を使ってみた

今回はフルマネージドなサーバーレスRDBである、PlanetScaleをNext.jsで作成したアプリケーションから使ってみたいと思います
2022.09.29

西田@MADグループです

今回はフルマネージドなサーバーレスRDBである、 PlanetScaleNext.js で作成したアプリケーションから使ってみたいと思います

なお、この記事で作成するアプリケーションのソースコードはこちらのリポジトリにアップロードしてます

PlanetScale とは?

PlnetScale は MySQL互換のサーバーレスデーターベースです

シームレスに水平スケーリングでき、その裏側として Vitess が採用されています

Vitess は Youtube や Slack でも採用された実績があるオープンソースデータベースです

PlanetScale の主な特徴

  • 設定なしで水平スケールが可能で、大規模なデータやトラフィックに対応
  • ダウンタイムなしのテーブル定義変更
  • Git のようなブランチ機能
  • サーバーレス

PlanetScale にサインアップ

ます PlanetScale にアカウントを以下のページから作成します

https://auth.planetscale.com/sign-up

※ 執筆時点では Github と Email でのサインアップが可能でした

今回の構成

  • Next.js
  • Prisma
  • PlanetScale

データベースを作成

データベースを作成します

任意の名前を入力して、リージョンを選択します

リージョンには ap-northeast-1 (東京) を選択しました

Next.js のセットアップ

Next.js のプロジェクトを作成します

今回は TypeScript で作成し、 src 配下に pages と styles を配置します

$ npx create-next-app --typescript sample
$ cd sample
$ mkdir src
$ mv pages styles src/

この時点で一度動作確認をしておきます

npm run dev もしくは yarn dev をターミナルで実行して、ブラウザで http://localhost:3000 にアクセスします

$ npm run dev

Prisma をセットアップ

今回はORMとして Prisma を使用します

npx prisma init で必要なファイルを生成し、Prisma を使って PlanetScale に接続するために必要な設定をします

prisma initコマンドで必要なファイルを生成

$ npx prisma init

prisma initコマンドで生成された prisma/schema.prismaファイルを編集

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

  // 以下の一文を追加
    previewFeatures = ["referentialIntegrity"]
}

datasource db {
  // provider に mysql を指定
  provider = "mysql"

  url      = env("DATABASE_URL")

  // 以下の一文を追加
  referentialIntegrity = "prisma"
}

referentialIntegrity を有効にする理由

Prisma ではMongoDB 以外のコネクタの場合、デフォルトで外部キー制約を使って、参照整合性を担保します

ただし、 PlanetScale では外部キー制約をサポートしません

Prisma で PlanetScale を使用する際は、referentialIntegrity を “prisma” にし外部キー制約を使用しないようにします

参考: Setting the referential integrity

PlanetScale CLI をインストール

PlanetScale をCLIで操作するためのコマンドを pscale をインストールします

$ brew install planetscale/tap/pscale
$ brew install mysql-client

※ 上記は Mac でのインストール方法です。その他のOSは以下を参考にしてください

参考: PlanetScale CLI Installation

PlanetScale にサインインしてCLIを利用する準備をします

$ pscale auth login

pscale auth loginコマンドを実行するとブラウザが起動し Confirmation Code を確認するページに遷移するので、ターミナルに表示された、 Confirmation Code と一致してるか確認して「Confirm code」をクリックします

※ mysql-client がまだ未インストールの場合

pscale コマンドでクエリを実行するのに mysql-client が必要なので、まだインストールしていない場合はインストールします

$ brew install mysql

※ 上記は Mac でのインストール方法です。

PlanetScale への接続をプロキシ

PlanetScale CLI を使って、ローカルマシンの 3309ポート(任意) に PlanetSclae への接続をプロキシし、ローカルマシンの 3309番ポートから PlanetScale に接続できるようにします

$ pscale connect $DB_NAME main --port 3309

$DB_NAME にはデータベースを作成時に設定した Name (この記事なら test-branch)を指定します

PlanetScale の接続情報を設定

.env ファイルにPlanetScaleへの接続文字列を記述し、環境変数として参照できるようにします

DATABASE_URL="mysql://root@127.0.0.1:3309/$DB_NAME"

Prisma のスキーマを定義

prisma/schema.prismaファイルにスキーマを追記し、Inquiry モデルを定義します

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

Prisma のスキーマを PlanetScale に反映

Prisma のスキーマを PlanetScale に反映します

この操作でデータベースとテーブルが作成されます

$ npx prisma db push

スキーマの反映を確認

psclae shell コマンドでPlanetScale上で動作中のデータベースにアクセスし、スキーマが反映されたのを確認します

$ pscale shell $DB_NAME main

$DB_NAME/main> show tables;
+-----------------------+
| Tables_in_test-branch |
+-----------------------+
| Inquiry               |
+-----------------------+

$DB_NAME/main> desc Inquiry;
+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| id      | int          | NO   | PRI | NULL    | auto_increment |
| name    | varchar(191) | NO   |     | NULL    |                |
| email   | varchar(191) | NO   |     | NULL    |                |
| subject | varchar(191) | NO   |     | NULL    |                |
| message | varchar(191) | NO   |     | NULL    |                |
+---------+--------------+------+-----+---------+----------------+

Next.js の API Route でデータを登録するAPIを作成

プロジェクト内のファイルをインポートするときに、プロジェクトのルートディレクトリからの絶対パスでインポートできるよう tsconfig.jsonを変更します

{
  "compilerOptions": {
        // 省略...
        "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
}

src/libs/database.tsファイルを作成し、データベースにデータを登録する関数を作成します

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

const prisma = new PrismaClient()

export type InquiryInput = {
    name: string,
    email: string,
    subject: string,
    message: string,
}

export async function createInquiry(params: InquiryInput) {
    return await prisma.inquiry.create({data: params});
}

src/pages/api/inquiries/index.tsファイルを作成し Inquiry を登録するAPI(POST /api/inquiries) を作成します

import { createInquiry } from "@/libs/database";
import type { NextApiRequest, NextApiResponse } from "next";

const postInquiry = async (req: NextApiRequest, res: NextApiResponse) => {
    const body = req.body

    const inquiry = await createInquiry({
        name: body.name,
        email: body.email,
        subject: body.subject,
        message: body.message,
    })

    return res.status(200).json(inquiry);
}

export default async (req: NextApiRequest, res: NextApiResponse) => {
    switch(req.method) {
        case "POST":
            return await postInquiry(req, res);
        default:
            return res.status(405).json({message: "Method not allowed"})
    }
}

参考: Next.js API Route (TypeScript)

curl コマンドを使って動作確認をします

$ curl -XPOST -H "content-type: application/json" localhost:3000/api/inquiries -d "{\"name\": \"abc\", \"email\":\"abc@example.com\",\"subject\": \"sample\",\"message\":\"sample message\"}"

> {"id":1,"name":"abc","email":"abc@example.com","subject":"sample","message":"sample message"}⏎

Next.js の API Route でデータを取得するAPIを作成

src/libs/databse.tsにデータベースからデータを取得する関数を追加します

export async function fetchInquiries() {
    return await prisma.inquiry.findMany();
}

src/pages/api/inquiries/index.tsファイルに Inquiry を取得するAPI (GET /api/inquiries) を追加します

export default async (req: NextApiRequest, res: NextApiResponse) => {
    switch(req.method) {
        case "POST":
            return await postInquiry(req, res);
        case "GET":
            return await getInquiries(req, res);
        default:
            return res.status(405).json({message: "Method not allowed"})
    }
}

作成したAPIを使って画面表示します

今回は Tailwind CSS を使用してスタイルを設定するので、その準備をします

$ npm install -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p

tailwind.config.jsを編集

module.exports = {
    mode: "jit",
    content: [
        "./src/**/*.{js,ts,jsx,tsx}",
    ],
    theme: {
        extend: {},
    },
    plugins: [],
}

続いてデータ取得ライブラリの SWR をインストールします

$ npm install swr

src/pages/index.tsx を以下のように変更します

主に以下の処理をしています

  1. 先ほど作成したデータ取得 API を呼び出し
  2. 取得したデータを使用してテーブルのデータ行部分をレンダリング
import type { NextPage } from 'next'
import useSWR from 'swr'
import { Inquiry } from '@prisma/client'

type Inquiries = Array<Inquiry>

const fetcher = (url: string) => fetch(url).then(res => res.json())

const Home: NextPage = () => {

  const {data, error} =  useSWR<Inquiries>("/api/inquiries", fetcher);

  if (error) return <div>Error has oocurred</div>
  if (!data) return <div>Now loading...</div>

  return (
    <div className="container mx-auto">
      <table className="w-full text-sm text-left text-gray-500 mt-3">
        <thead className="text-xs text-gray-700 bg-gray-50">
          <tr>
            <th className="py-3 px-6">Name</th>
            <th className="py-3 px-6">Email</th>
            <th className="py-3 px-6">Subject</th>
            <th className="py-3 px-6">Message</th>
          </tr>
        </thead>
        <tbody>
          {data.map((inquiry) => {
            return (
              <tr className="bg-white border-b" key={inquiry.id}>
                <td className="py-4 px-6">{inquiry.name}</td>
                <td className="py-4 px-6">{inquiry.email}</td>
                <td className="py-4 px-6">{inquiry.subject}</td>
                <td className="py-4 px-6">{inquiry.message}</td>
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}

export default Home

作成したAPIを使って画面から登録できるようにします

以下のコードを src/pages/index.tsxに書き加えます。

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");

const clearForm = () => {
  setName("");
  setEmail("");
  setSubject("");
  setMessage("");
}

const {data, error, mutate} =  useSWR<Inquiries>("/api/inquiries", fetcher);

const onSubmit = async (e: React.SyntheticEvent) => {
  e.preventDefault();

  const response = await fetch("/api/inquiries", {method: "POST", body: JSON.stringify({
    name, email, subject, message
  }),
  headers: {
    "Content-Type": "application/json"
  }})

  clearForm();
  mutate();
}

SWRはローカルにキャッシュを持つので、更新処理後に即座に画面に反映させるには、mutate 関数を使って、SWR のローカルキャッシュをクリアし、再度データを取得させる必要があります

const {data, error, mutate} =  useSWR<Inquiries>("/api/inquiries", fetcher);
mutate();

src/pages/index.tsxに以下をフォームを書き加えます

<form action="" className="bg-white rounded px-8 shadow-md mt-3 p-3" onSubmit={onSubmit}>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Name
    </label>
    <input onChange={(e) => setName(e.target.value)} type="text" value={name} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Email
    </label>
    <input onChange={(e) => setEmail(e.target.value)} type="text" value={email} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Subject
    </label>
    <input onChange={(e) => setSubject(e.target.value)} type="text" value={subject} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Message
    </label>
    <textarea onChange={(e) => setMessage(e.target.value)} value={message} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="flex items-center justify-between">
    <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Post</button>
  </div>
</form>

アプリケーションを Vercel にデプロイ

最後に今回作成したアプリケーションを Vercel で動かしてみます

PlanetScale でデータベース接続情報を取得

PlanetScale でブランチを選択し、「Connect」ボタンを押します

「New Password」 をクリックしデータベースへ接続するためのパスワードを生成し、 「Connect With」に Prismaを選択して、データベース接続文字列を表示させます

コードを Github に Push

Vercel にデプロイできるようにするため、今回作成したコードを Github に Push します

$ git add -A
$ git commit -m "initial commit"
$ git push origin main

Vercel にアカウントを作成

Vercel にアカウントを作成します。

Vercel にデプロイするリポジトリをインポート

今回は Github のリポジトリをデプロイするため、Githubからリポジトリをインポートします

今回作成したリポジトリを選択します

プロジェクト設定の Environment Variables に DATABSE_URL という名前で、先ほど PlanetScale で表示したデータベース接続情報文字列 mysql://で始まる部分を抜き出し設定します

そのあと「Deploy」をクリックします

デプロイしたプロジェクトのドメインにアクセスし

画面が見えれば完了です

参考

How to set up Next.js with Prisma and PlanetScale