RIT Tech Blog

株式会社RITのエンジニアが知見を共有する技術ブログです。

Prisma × PlanetScale × Netlify で PlanetScale入門

エンジニアの岸本です。 現在、総額1億500万ドル(約120億円)を調達したことで話題になった、「PlanetScale」というサーバーレスデータベースを皆さんご存知ですか? docs.planetscale.com

今回は簡易掲示板を実際に作りながら、PlanetScaleの導入から使用方法を共有したいと思います。 ※アカウント作成等の手順は、以下記事を参考にすることをおすすめします。 qiita.com

構成

  • ビルドツール
    • Vite
  • DB
    • PlanetScale
  • server
    • Netlify
  • アプリケーション

※ preact の代わりにNext.jsを使っても手順に大差はないので、適宜ご自身の仕様と比較しながら読み進めてもらえればと思います。

始める前に結論と所感

  • 無料枠が大きいので個人開発で遊ぶのにも十分だと思う
  • UXがとても良い。ほとんどのプログラマーgithubユーザーだと思うので、直感的に操作することが可能
  • mysqlが抱えていた問題(本番DBと開発DBの情報に差分が発生して意図しないバグを生じてしまう)を上手く解決してくれそう(上記に記載したように、githubのようなユーザー体験が提供されているためschema変更のrevartなんかも可能)

始める前の準備

PlanetScaleNetlifyはそれぞれcliが用意されているのでインストールします

$ npm install netlify-cli -g
$ netlify // インストールされているか確認

⬥ Netlify CLI
Read the docs: https://www.netlify.com/docs/cli
Support and bugs: https://github.com/netlify/cli/issues

VERSION
  netlify-cli/9.16.6 darwin-x64 node-v14.17.0


$ npm i scale -g
$ which sclae

Vite プロジェクト作成

適当なディレクトリを作成します

$ mkdir ディレクトリ名
$ cd ディレクトリ名
$ npm init vite@latest .

プロジェクト名等、何を利用するか問われるので以下の項目を入力・選択します

$ npm init vite@latest
npx: 6個のパッケージを3.977秒でインストールしました。
✔ Project name: … プロジェクト名
✔ Select a framework: › preact
✔ Select a variant: › preact-ts

$ cd プロジェクト名
$ npm i

プロジェクト作成完了後、ローカルサーバを走らせます

$ netlify dev

◈ Netlify Dev ◈
◈ Ignored general context env var: LANG (defined in process)
◈ Starting Netlify Dev with Vite

   ┌─────────────────────────────────────────────────┐
   │                                                 │
   │   ◈ Server now ready on http://localhost:8888   │
   │                                                 │
   └─────────────────────────────────────────────────┘

補足:) netlify devコマンドに関しては以下を参照ください

www.netlify.com

http://localhost:8888でプロジェクトが正常に立ち上がり以下の画面が確認できるはずです。

ブラウザ画面
viteプロジェクト初期画面

PlanetScale の設定

ターミナルからPlanetScaleloginします

$ pscale auth login
 Confirmation Code: ここに認証用コードが表示される

ブラウザでplanetscale認証ページが自動で開かれるので、ターミナル上に出力される認証用コードと一致しているか確認してください。 問題なければブラウザ上に表示されている、Confirme codeボタンをクリックします

次に、開発用のブランチを作成します

$ pscale branch create データベース名 dev // devは任意のブランチ名

※ ブランチ機能があり、GitのようにDBを管理できる所が良いですよね

shadowブランチを作成する ※ このブランチはPrismaのmigrations用に使われる

$ pscale branch create データベース名 shadow // devは任意のブランチ名

ローカルからデータベースへ接続して、正常に上記設定が行われているか確認します

$ pscale connect データベース名 dev[ブランチ名] --port 3309
$ pscale connect データベース名 shadow[ブランチ名] --port 3310

エディタで本プロジェクトコードを展開して.envファイルを追加して以下を記述します

DATABASE_URL="mysql://root@127.0.0.1:3309/データベース名"
SHADOW_DATABASE_URL="mysql://root@127.0.0.1:3310/データベース名"

Prismaの設定

プロジェクトにprismaを追加します

npm i -D prisma
npm i @prisma/client

schemaファイルを生成します

npx prisma init

schema.prismaを以下のように修正

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

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

/* 以下、投稿情報用モデルです */
model Post {
  id Int @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now())
  title String @db.VarChar(255)
  content String
}

上記で定義した schema を基にテーブルを作成します

$ npx prisma migrate dev

無事に作成できたらprisma studioを使用して検証用データを作成します

$ npx prisma studio

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555 /* ブラウザでhttp://localhost:5555を開く */

prisma studio 画像
prisma_studio

schemaファイルを基に prisma client を生成します npx prisma generateコマンドはスキーマを取得し、typescript定義を持つprismaクライアントを作成して、データベースとのやり取りに必要なものを自動で準備してくれるコマンドです

$ npx prisma generate

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (3.12.0 | library) to ./node_modules/@prisma/client in 70ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

serverless functionを定義

プロジェクトのルートディレクトリにnetlify/functions/posts.tsを作成します. ファイル内は以下のような構成にします. 処理の内容は post した情報をDBから取得して返すだけのシンプルなものになってます.

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function handler() {
  try {
    const posts = await prisma.post.findMany();
    return {
      statusCode: 200,
      header: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(posts),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(error),
    };
  }
}

ちなみに、Netlify の Netlify function とは Netlify が提供しているアドオンの1つで、サーバーサイドの機能を簡単に公開できるサービスです。AWS Lambda をラップして使いやすいように提供されており、 AWSにアカウントを用意する必要もありません。NetlifyのプロジェクトとGitリポジトリを連携するだけでOK。Netlifyが代わりにデプロイをしてくれる上に、HTTPで呼び出せる機能がすぐに用意できる代物です。

上記コードが正常に機能するかテストしましょう. ローカルサーバを起動します(もし起動していなければ).

$ netlify dev
◈ Netlify Dev ◈
◈ Ignored build settings env var: DATABASE_URL (defined in .env file)
◈ Injected .env file env var: DATABASE_URL
◈ Ignored general context env var: LANG (defined in process)
◈ Injected .env file env var: SHADOW_DATABASE_URL
◈ Loaded function post.
◈ Loaded function posts.
◈ Functions server is listening on 62987
◈ Starting Netlify Dev with Vite

> prisma-serverless@0.0.0 dev /Users/ryoma_kishimoto/Desktop/prisma-serverless-planetscale-netlify/prisma-serverless
> vite


  vite v2.9.5 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

  ready in 540ms.


   ┌─────────────────────────────────────────────────┐
   │                                                 │
   │   ◈ Server now ready on http://localhost:8888   │
   │                                                 │
   └─────────────────────────────────────────────────┘

posts を返すか確認します.

http://localhost:8888/.netlify/functions/posts

レスポンスが確認できたらOKです.

次は実際にデータを post する処理をプロジェクトのルートディレクトリ(netlify/functions/post.ts)に作成しましょう.

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function handler(event) {
  const { title, content } = JSON.parse(event.body);
  try {
    await prisma.post.create({
      data: { title, content },
    });
    return {
      statusCode: 200,
      body: 'post created',
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(error),
    };
  }
}

正常に起動するか確認します. VSCode拡張機能でThunder Clientという優れものがあるので、検証機能としておすすめです. https://www.thunderclient.com/

簡単にですが使い方を紹介します.

ブラウザからのPOST機能実装

以下のコードをそのままsrc/app.tsxへcopy & pasteしちゃいましょう.

import { useState, useEffect } from 'preact/hooks';

type Post = {
  id: number;
  title: string;
  content: string;
  createdAt: Date;
  updatedAt: Date;
};

export function App() {
  const [loadPosts, setLoadPost] = useState(true);
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    async function load() {
      if (!loadPosts) {
        return;
      }
      const allPosts = await fetch('/.netlify/functions/posts').then((res) =>
        res.json()
      );
      setPosts(allPosts);
      setLoadPost(false);
    }
    load();
  }, [loadPosts]);

  async function handleSubmit(event: any) {
    event.preventDefault();

    await fetch('/.netlify/functions/post', {
      method: 'POST',
      body: JSON.stringify({ title, content }),
    });
    setTitle('');
    setContent('');
    setLoadPost(true);
  }

  return (
    <>
      <h1>投稿画面</h1>
      <ul>
        {posts.map((post: Post) => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>Created {new Date(post.createdAt).toLocaleString()}</p>
            <p>Updated {new Date(post.updatedAt).toLocaleString()}</p>
          </li>
        ))}
      </ul>
      <h2>投稿しよう</h2>
      <form onSubmit={handleSubmit}>
        <label htmlFor='title'>Title</label>
        <input
          type='text'
          id='title'
          name='title'
          value={title}
          onChange={(e) => setTitle((e.target as HTMLInputElement).value)}
        />

        <label htmlFor='content'>content</label>
        <input
          type='content'
          id='content'
          name='content'
          value={content}
          onChange={(e) => setContent((e.target as HTMLInputElement).value)}
        />

        <button type='submit'>Save</button>
      </form>
    </>
  );
}

一応、cssも記述します.

html,
body {
  font-family: 'Helvetica Neue', arial, sans-serif;
}
button,
label {
  display: block;
  margin-top: 1rem;
}

以下画面がブラウザ上で確認できるはずなので、正常に「投稿→投稿されたPOSTが表示」されるか挙動チェックしましょう.

PlanetScaleのProduction環境DBの準備

PlanetScale のコンソール画面上からmainブランチを選択します.

mainブランチをproduction用としてpromote

任意のブランチをmainブランチにdeployするために、deploy requestを上げます.(gitのpull requestに近いイメージ) ターミナルから以下コマンドを入力

pscale deploy-request create データベース名 dev // devは任意のブランチ名

PlanetScaleのコンソール画面上にある、「Deploy requests」を選択すると、deploy requestが確認できます.

summaryタブから「Add changes to deploy queue」ボタンをクリックします.

Netlifyへアプリケーションをデプロイ

丁寧にまとめられている、以下記事を参考にして下さい. https://qiita.com/suin/items/743fe6252ad8af425c5e

「Site settings」をクリックし、画面左に表示されている「Build & deploy」タブを選択します.

「Build settings」項目で以下のように設定し、下部の「save」ボタンをクリックします.

PlanetScaleからDBへのURLを取得します. 次にコンソール画面上の「overview」タブを選択し、画面右の「connect」ボタンをクリックします.

「connect with」ドロップダウンメニューから「Prisma」を選択し、画面右のコピーアイコンをクリックしたら適当なメモ帳等にペーストしておきましょう.

Netlifyへ戻り、「Site settings」をクリック、画面左に表示されている「Build & deploy」タブを選択して、以下スクショのように設定しますvalueには先ほどコピペした、DATABASE_URL情報のここから→mysql://~をペーストする)

これで終了です. もし正常にdeployされていない場合は、「Deploy」タブから「Trigger deploy」ボタンをクリックして再度、deployを行なって下さい.

補足:) 以下スクショ内のリンクをクリックすると、Netlify にデプロイされたページへ遷移することができます.