Next.js で PlanetScale を使ってみた
西田@MADグループです
今回はフルマネージドなサーバーレスRDBである、 PlanetScale を Next.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
を以下のように変更します
主に以下の処理をしています
- 先ほど作成したデータ取得 API を呼び出し
- 取得したデータを使用してテーブルのデータ行部分をレンダリング
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」をクリックします
デプロイしたプロジェクトのドメインにアクセスし
画面が見えれば完了です