😎

Next.js(App Router) x PlanetScale x Drizzle x Vercel でエッジからDBにアクセス

2023/06/15に公開
2

はじめに

  • Next.js(App Router)、PlanetScale、Drizzle, Vercel でプロジェクトを構築する方法を紹介します。
  • PlanetScale でアカウントを作成し、実際にデータベースを作成します。
  • Drizzle を通して、テーブルを作成、データを投入します。
  • Next.js の App Router を使って、データベースのデータを取得、表示します。
  • Vercel のエッジ環境で動作させます。
  • さらに、PlanetScale で商用ブランチと開発ブランチの使い方について紹介します。

本記事で作成したソースコードは以下にあります。

https://github.com/hayato94087/nextjs-planetscale-drizzle

なお、Next.js(App Router)、PlanetScale、Prisma について興味ある方はこちらを参照ください。

https://zenn.dev/hayato94087/articles/710c3096cc9708

動機

以下が本記事を作成した動機です。

  • Vercel のエッジからデータベースにアクセスしたい
  • 商用ブランチと開発用ブランチを分けられる PlanetScale を使ってみたい

Drizzle はドキュメントが少ないので、構築するのに手こずりました。本記事では PlanetScale を使う前提でかなり丁寧に説明しています。

PlanetScaleについて

PlanetScale とは、クラウドネイティブな MySQL データベースです。シームレスなスケーリング、高可用性、セキュリティを備えています。裏側としては、Vitessというオープンソースのデータベースが採用されています。

PlanetScale の特出すべき特徴は以下の2点です。

  • MySQL を利用したサーバレスな DB
  • ブランチ機能によるデータベースのバージョン管理

特にブランチ機能は PlanetScale ならではの強みで、商用 DB から、開発 DB を作成できます。開発 DB でのテストが終わったら、ブランチをマージすることで、本番 DB に反映させることができます。本記事でも取り上げています。

https://planetscale.com/

Drizzle

Drizzle は Prizma と同じく、SQL を書かずに、データベースを操作するためのライブラリ、ORM、です。

https://orm.drizzle.team/

最近、アプリケーションをエッジで動作するニーズが高まり、エッジで動作することが難しい Prisma にかわり、エッジで動作できる Drizzle に注目が集まっています。私は、以下の動画を見て、Drizzle に興味を持ちました。

https://www.youtube.com/watch?v=8met6WTk0mQ

https://www.youtube.com/watch?v=_SLxGYzv6jo

それでは、実際の環境を作りながら、説明していきます。

新規にNext.js プロジェクトを作成

プロジェクトを新規に作成します。next-app@latest で、最新のバージョンを利用します。App Router を利用するため、--app を指定しています。

$ pnpm create next-app@latest nextjs-planetscale-drizzle --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app

プロジェクトディレクトリーに移動します。

$ cd nextjs-planetscale-drizzle

以下の通り修正し、テストページを作っておきます。

src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/layout.tsx
import './globals.css'

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body className="bg-white">{children}</body>
    </html>
  )
}
src/app/page.tsx
export const runtime = "edge";

export default function Home() {
  return (
    <main>
      <h1>テストページ</h1>
    </main>
  )
}

ローカルで実行します。

$ pnpm dev

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "create nextjs project"

GitHubのリポジトリを作成

後の GitHub のリポジトリを Vercel に連携させるため、ソースを GitHub リポジトリで管理します。

GitHub のリポジトリを作成します。

リポジトリにプッシュします。

$ git remote add origin https://github.com/hayato94087/nextjs-planetscale-drizzle.git
$ git branch -M main
$ git push -u origin main

Vercelにデプロイ

GitHub のリポジトリを連携させ、Vercel にデプロイします。

Vercel にログインし、「Add New」→「Project」をクリックします。

対象となるリポジトリを選択し、「Import」をクリックします。

「Deploy」をクリックします。

デプロイが完了しました。ダッシュボードにアクセスします。

念のためデプロイされた環境にアクセスするため、「Visit」をクリックします。

無事、デプロイできました。

PlanetScaleのアカウントを作成

アプリケーションのデータは PlanetScale のデータベースで管理します。ここでは、PlanetScale のアカウントを作成します。

PlanetScale のサイトにアクセスします。

https://planetscale.com/

「Get started」をクリックします。

「Continue with GitHub」をクリックします。アカウントは手持ちの GitHub アカウントで簡単に作成できます。

自身が持っている GitHub アカウントでログインします。

「Authorize planetscale」をクリックします。

「Accept Terms of Service」をクリックします。

この画面に到達すると、自身のメールアドレスに確認メールが届いていますので、メールボックスを確認します。

「Confirm email」をクリックし、アカウント作成を完了させます。

アカウント作成が完了しました。

PlanetScaleでデータベースを作成

続いて、データベースを作成していきます。

初めにチュートリアルが表示されるので、ボタンをクリックして次へ進めていきます。

これでチュートリアル完了です。「Create your first database」をクリックして、データベースを作成します。

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

  • Name にはデータベース名を記入します。
  • Regionap-northeast-1(Tokyo) を選択します。これで、東京リージョンにデータベースを作成できます。
  • 最後に、「Create database」をクリックします。

しばらくすると、データベースの作成が完了します。

PlanetScaleでデータベースの接続先を取得

「Get connection strings」をクリックします。

「Create password」をクリックします。

接続先の情報が表示されました。Password はこの画面でしか確認できないので、メモしておきます。

続いて、「Connection with」を「Prisma」に設定すると、環境変数ファイル(.env)でデータベースの接続先の情報が確認できます。DATABASE_URL をメモしておきます。今回は、Prisma ではなく、Drizzle を ORM として利用しますが、Drizzle でもこの接続先の情報を利用できます。

環境変数ファイルに接続先の情報を設定

PlanetScale の管理コンソールで取得したデータベースへの接続先情報(DATABASE_URL)を Next.js の環境変数ファイル(.env)に設定します。

ファイルを作成します。

$ touch .env
.env
DATABASE_URL='mysql://xxmkedtjlbdobjdm26mc:pscale_pw_kjTGyAzsCBTeJfUzeDfUwCOa3eNxIlC4COseqt3RAea@aws.connect.psdb.cloud/nextjs-planetscale-drizzle?sslaccept=strict'```

.gitignore を修正し、.env を Git の管理対象から外します。

.gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
+.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add .env to .gitignore"

Drizzleのパッケージをインストール

Drizzle のパッケージをインストールします。drizzle-ormは、Drizzle のコアパッケージです。drizzle-kitは、Drizzle の CLI ツールです。drizzle-kit を利用することで、スキーマファイルを利用してテーブルの作成/更新に必要な、マイグレーションファイルを作成できます。

$ pnpm add drizzle-orm
$ pnpm add -D drizzle-kit

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add drizzle packages"

Drizzleでデータベースのスキーマファイルを作成

データベースのテーブルを定義するスキーマファイルを作成します。スキーマファイルは複数のファイルに分割して管理できます、1 つのファイルにまとめることができます。公式は 1 つのファイルに纏めることを推奨しています。

1つのファイルに場合(公式推奨)
📦 <project root>
 └ 📂 src
    └ 📂 db
       └ 📜schema.ts
複数ファイルに分割している場合
📦 <project root>
 └ 📂 src
    └ 📂 db
       └ 📂 schema
          ├ 📜users.ts
          ├ 📜countries.ts
          ├ 📜cities.ts
          ├ 📜products.ts
          ├ 📜clients.ts
          ├ 📜enums.ts
          └ 📜etc.ts

スキーマファイルを作成します。

$ mkdir -p src/db
$ touch src/db/schema.ts
src/db/schema.ts
import { mysqlTable, serial, text, int, index } from "drizzle-orm/mysql-core";
import { InferModel, relations } from "drizzle-orm";

export const users = mysqlTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull(),
  name: text("name").notNull(),
});

export const posts = mysqlTable(
  "posts",
  {
    id: serial("id").primaryKey(),
    title: text("title").notNull(),
    content: text("content").notNull(),
    authorId: int("author_id").notNull(),
  },
  (table) => ({
    authorIdIndex: index("authorId_idx").on(table.authorId),
  })
);

export const userRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

export type User = InferModel<typeof users>;
export type NewUser = InferModel<typeof users, "insert">;
export type Post = InferModel<typeof posts>;
export type NewPost = InferModel<typeof posts, "insert">;

posts テーブルについては author_id に対してインデックスを作成しています。インデックスを作成することで、author_id を利用した検索が高速になります。SQL については、ここではとりあげませんが、インデックスについてこちらの記事や動画が参考になります。

https://zenn.dev/hk_206/articles/ec5f4e347caff4

SQL を速くするインデックス入門 : B-Tree や複合インデックスが理解できる

そもそも SQL の基礎を理解したい場合は、インデックスの説明ありませんが以下の動画でざっくり理解できます。

https://www.youtube.com/watch?v=v-Mb2voyTbc

DrizzleとPrismaでのスキーマの記述方法の違いを確認

Prisma と Drizzle ではスキーマファイルの記述方法が異なります。

Prisma の場合は、Prisma独自記法でスキーマファイルを記述します。

Prismaの場合の記述例
// Prismaの場合

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

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

  @@index([authorId])
}

一方、Drizzle は開発者が慣れているTypeScriptでスキーマファイルを記述できます。あらためて、Drizzle でのスキーマファイルの記述方法を記載します。

src/db/schema.ts
import { mysqlTable, serial, text, int, index } from "drizzle-orm/mysql-core";
import { InferModel, relations } from "drizzle-orm";

export const users = mysqlTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull(),
  name: text("name").notNull(),
});

export const posts = mysqlTable(
  "posts",
  {
    id: serial("id").primaryKey(),
    title: text("title").notNull(),
    content: text("content").notNull(),
    authorId: int("author_id").notNull(),
  },
  (table) => ({
    authorIdIndex: index("authorId_idx").on(table.authorId),
  })
);

export const userRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

export type User = InferModel<typeof users>;
export type NewUser = InferModel<typeof users, "insert">;
export type Post = InferModel<typeof posts>;
export type NewPost = InferModel<typeof posts, "insert">;

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add schema file"

Drizzleの設定ファイルを作成

Drizzle を設定ファイル(drizzle.config.ts)を作成し、設定を一元管理します。

  • schema は、スキーマファイル(schema.ts)のパスを指定
  • out は、schema の変更結果などをまとめたマイグレーションファイルの出力先のパスを指定
  • breakpoints は、true を指定
  • 他のファイルから参照できるように、drizzleConfigexport
$ touch drizzle.config.ts
drizzle.config.ts
import type { Config } from "drizzle-kit";

const drizzleConfig = {
  schema: "./src/db/schema.ts",
  out: "./src/db/migrations",
  breakpoints: true,
} satisfies Config;

export default drizzleConfig;

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add drizzle config file"

Drizzleでマイグレーションファイルを作成

スキーマファイルをベースにマイグレーションファイルを生成します。マイグレーションファイルとは、データベースにどのような変更をすべきか記載されたファイルです。マイグレーションファイルを生成することで、データベースの変更をコードで管理できます。

MySQL を採用している PlanetScale の場合は、マイグレーションファイルの作成に、drizzle-kit generate:mysql コマンドを実行します。PostgreSQL の場合は、コマンドが若干異なります。drizzle-kit generate:mysql が、どのような引数を受け付けるか --help で確認できます。drizzle-kit generate:mysql の詳細については、こちらの公式サイトでも確認ができます。

$ pnpm drizzle-kit generate:mysql --help

Usage: index generate:mysql [options]

Options:
  --schema <schema...>  Path to a schema file or folder
  --out <out>           Output folder, 'drizzle' by default
  --breakpoints         Prepare SQL statements with breakpoints
  --custom              Prepare empty migration file for custom SQL
  --config <config>     Path to a config.json file, drizzle.config.json by default
  -h, --help            display help for command

マイグレーションファイル作成のコマンドを実行します。--config で、Drizzle の設定ファイル(drizzle.config.ts)のパスを指定します。Drizzle の設定ファイル(drizzle.config.ts)からスキーマファイルのパスとマイグレーションファイルの出力先のパスが読み込まれます。

$ pnpm drizzle-kit generate:mysql --config drizzle.config.ts

drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

Reading config file '/Users/hayato94087/Private/nextjs-planetscale-drizzle/drizzle.config.ts'
Reading schema files:
/Users/hayato94087/Private/nextjs-planetscale-drizzle/src/db/schema.ts

2 tables
posts 4 columns 1 indexes 0 fks
users 3 columns 0 indexes 0 fks

[] Your SQL migration file ➜ src/db/migrations/0000_magical_slapstick.sql 🚀

以下が作成されたファイルです。合計 3 つのファイルが作成されています。

$ tree src/db/migrations

src/db/migrations
├── 0000_magical_slapstick.sql
└── meta
    ├── 0000_snapshot.json
    └── _journal.json

2 directories, 3 files

0000_magical_slapstick.sql にはデータベースにどのような操作が実行されるべきか SQL で記述されています。なお、この時点では、データベースにテーブルは生成されていません。

src/db/migrations/0000_magical_slapstick.sql
CREATE TABLE `posts` (
	`id` serial AUTO_INCREMENT PRIMARY KEY NOT NULL,
	`title` text NOT NULL,
	`content` text NOT NULL,
	`author_id` int NOT NULL);
--> statement-breakpoint
CREATE TABLE `users` (
	`id` serial AUTO_INCREMENT PRIMARY KEY NOT NULL,
	`email` text NOT NULL,
	`name` text NOT NULL);
--> statement-breakpoint
CREATE INDEX `authorId_idx` ON `posts` (`author_id`);

毎回、drizzle-kit generate:mysql を書くのは面倒なので、package.jsondb:generate のスクリプトを追加します。

package.json
{
  "scripts": {
+    "db:generate": "pnpm drizzle-kit generate:mysql --config drizzle.config.ts",
  },
}

今後は、以下のように実行できます。

$ pnpm db:generate

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add migration files and script to generate migration files"

Drizzleでデータベース接続クライアントを作成

マイグレーションを行うためにはデータベースに接続する必要があるため、データベースに接続するクライアントを作成します。

PlanetScale のサーバレスデータベースに接続するためのクライアントを作成します。公式サイトに、クライアント作成方法が記載されています。PlanetScale に接続するためのドライバー、@planetscale/databaseをインストールします。また、.env から DATABASE_URL を読み込むため、dotenv-cli もインストールします。

$ pnpm add @planetscale/database
$ pnpm add dotenv-cli -D
$ touch src/db/index.ts
src/db/index.ts
// db.ts
import { drizzle } from "drizzle-orm/planetscale-serverless";
import { connect } from "@planetscale/database";
import * as schema from "./schema";

// create database connection
const connection = connect({
  url: process.env.DATABASE_URL,
});

export const db = drizzle(connection, { schema });

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add drizzle client and drizzle-client related packages"

Drizzleのマイグレーションを実行するコードの作成

作成したマイグレーションファイルを使ってデータベースを更新するためのプログラム、マイグレーションを実行するためのプログラム、を作成します。プログラムはJoscha Neskeコードを参考にしています。fetchのパッケージをインストールします。

$ touch src/db/migrate.ts
$ pnpm add undici
src/db/migrate.ts
import { migrate } from "drizzle-orm/planetscale-serverless/migrator";
import { connect } from "@planetscale/database";
import { drizzle } from "drizzle-orm/planetscale-serverless";
import drizzleConfig from "../../drizzle.config";
import { fetch } from "undici";

const runMigrate = async () => {
  // データベースの接続先情報を取得できなければエラーで終了
  if (!process.env.DATABASE_URL) {
    throw new Error("DATABASE_URL is not defined");
  }

  const connection = connect({
    url: process.env.DATABASE_URL,
    fetch,
  });

  // データベースに接続するクライアントを作成
  const db = drizzle(connection);

  console.log("⏳ Running migrations...");

  // 開始時間をメモ
  const start = Date.now();

  // マイグレーションを実行
  // マイグレーションファイルは、drizzle.config.tsのoutに指定したフォルダに作成されます。
  await migrate(db, { migrationsFolder: drizzleConfig.out });

  // 終了時間をメモ
  const end = Date.now();

  // 事項時間を出力
  console.log(`✅ Migrations completed in ${end - start}ms`);

  process.exit(0);
};

runMigrate().catch((err) => {
  console.error("❌ Migration failed");
  console.error(err);
  process.exit(1);
});

マイグレーションを実行するためのスクリプトを package.json に追加します。.env を読み込むために、dotenv を利用します。dotenv-cli の使い方については、こちらをご確認ください。

package.json
{
  "scripts": {
+    "db:push": "pnpm with-env node -r esbuild-register src/db/migrate.ts",
+    "with-env": "dotenv -e .env --"
  },
}

今回は ts-node のかわりに esbuild-register を利用するので、パッケージをインストールします。esbuild-registerについてはこちらを確認ください。

$ pnpm add -D esbuild-register

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add code to perform migration and also add related packages required for migration program"

Drizzleでデータベースのマイグレーションを実行

マイグレーションのスクリプトを実行し、データベースを更新します。

$ pnpm db:push

> nextjs-planetscale-drizzle@0.1.0 db:push /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/migrate.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/migrate.ts"

⏳ Running migrations...
✅ Migrations completed in 398ms

実際にテーブルが登録されたかデータベースを確認します。PlanetScale の管理画面に移動し、「Tables」をクリックします。

postsusers のテーブルが表示されていることから、無事テーブルが作成されていることがわかります。

PlanetScaleのコンソールでデータを登録

テーブルは作成されましたが、データはまだ登録されていません。ここでは、PlanetScale のコンソールからデータを登録します。

念のためデータが入っていないことを PlanetScale のコンソールから確認します。「Console」をクリックします。

「Connect」をクリックし、コンソールに接続します。

続いて、以下の SQL をコンソールに投入して、結果を確認します。テーブルが空であることを確認できました。

select * from users;
select * from posts;

では、適当にデータを作成したので、以下のデータを登録してみます。

# user
INSERT INTO users (name, email) VALUES ('山田太郎', 'yamada@example.com');
INSERT INTO users (name, email) VALUES ('田中花子', 'tanaka@example.com');

# post
INSERT INTO posts (title, content, author_id) VALUES ('6月6日(山田太郎)', 'Hello World', 1);
INSERT INTO posts (title, content, author_id) VALUES ('6月7日(山田太郎)', 'Hello Universe', 1);
INSERT INTO posts (title, content, author_id) VALUES ('6月6日(田中花子)', 'Hello', 2);

以下の SQL をコンソールに投入して、テーブルにデータが入っていることを確認します。

select * from users;
select * from posts;

データが無事投入されていることが確認できました。

Drizzleを利用してデータをローカルで確認

Drizzle を利用しデータを確認します。

データベースからデータを参照するコードを作成します。

$ touch src/db/select.ts
src/db/select.ts
import { db } from ".";
import { posts, users } from "./schema";
import { eq, or, asc, desc } from "drizzle-orm";

async function main() {
  // すべてのユーザーを取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users;
  const selecAllUsers = await db.select().from(users);
  console.log("すべてのユーザーを取得");
  console.log(selecAllUsers);
  console.log("\n-----------------------------------\n");

  // すべての投稿を取得
  //
  // 以下と同等のSQL
  // SELECT * FROM posts;
  const selecAllPosts = await db.select().from(posts);
  console.log("すべての投稿を取得");
  console.log(selecAllPosts);
  console.log("\n-----------------------------------\n");

  // 1件のユーザーを取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users LIMIT 1;
  const selec1Users = await db.select().from(users).limit(1);
  console.log("1件のユーザーを取得");
  console.log(selec1Users);
  console.log("\n-----------------------------------\n");

  // ユーザー名が山田太郎のユーザーを取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users WHERE name = '山田太郎';
  const selecYamadaTaroUsers = await db
    .select()
    .from(users)
    .where(eq(users.name, "山田太郎"));
  console.log("ユーザー名が山田太郎のユーザーを取得");
  console.log(selecYamadaTaroUsers);
  console.log("\n-----------------------------------\n");

  // ユーザー名が山田太郎あるいは田中花子のユーザーを取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users WHERE name = '山田太郎' OR name = '田中花子';
  const selecSuzukiHanakoOrYamadaTaroUsers = await db
    .select()
    .from(users)
    .where(or(eq(users.name, "山田太郎"), eq(users.name, "田中花子")));
  console.log("ユーザー名が山田太郎あるいは田中花子のユーザーを取得");
  console.log(selecSuzukiHanakoOrYamadaTaroUsers);
  console.log("\n-----------------------------------\n");

  // すべてのユーザー、投稿とあわせて取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users LEFT JOIN posts ON users.id = posts.author_id
  const selecAllUsersAndPosts = await db
    .select({
      userId: users.id,
      userName: users.name,
      post: {
        postId: posts.id,
        postTitle: posts.title,
      },
    })
    .from(users)
    .leftJoin(posts, eq(users.id, posts.authorId))
    .orderBy(asc(users.id), desc(posts.id));
  console.log("すべてのユーザー、投稿とあわせて取得");
  console.log(selecAllUsersAndPosts);
  console.log("\n-----------------------------------\n");

  // すべてのユーザー、投稿とあわせて取得
  const queryAllUsersWithPosts = await db.query.users.findMany({
    with: {
      posts: true,
    },
  });
  console.log("すべてのユーザー、投稿とあわせて取得\n");
  console.log(JSON.stringify(queryAllUsersWithPosts, null, 2));
  console.log("\n-----------------------------------\n");
}

main();

package.json にスクリプトを登録します。

package.json
{
  "scripts": {
+    "db:select": "pnpm with-env node -r esbuild-register src/db/select.ts"
  }
}

プログラムを実行します。

$ pnpm db:select

> nextjs-planetscale-drizzle@0.1.0 db:select /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/select.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/select.ts"

すべてのユーザーを取得
[
  { id: 1, email: 'yamada@example.com', name: '山田太郎' },
  { id: 2, email: 'tanaka@example.com', name: '田中花子' }
]

-----------------------------------

すべての投稿を取得
[
  { id: 1, title: '6月6日(山田太郎)', content: 'Hello World', authorId: 1 },
  {
    id: 2,
    title: '6月7日(山田太郎)',
    content: 'Hello Universe',
    authorId: 1
  },
  { id: 3, title: '6月6日(田中花子)', content: 'Hello', authorId: 2 }
]

-----------------------------------

1件のユーザーを取得
[ { id: 1, email: 'yamada@example.com', name: '山田太郎' } ]

-----------------------------------

ユーザー名が山田太郎のユーザーを取得
[ { id: 1, email: 'yamada@example.com', name: '山田太郎' } ]

-----------------------------------

ユーザー名が山田太郎あるいは田中花子のユーザーを取得
[
  { id: 1, email: 'yamada@example.com', name: '山田太郎' },
  { id: 2, email: 'tanaka@example.com', name: '田中花子' }
]

-----------------------------------

すべてのユーザー、投稿とあわせて取得
[
  {
    userId: 1,
    userName: '山田太郎',
    post: { postId: 2, postTitle: '6月7日(山田太郎)' }
  },
  {
    userId: 1,
    userName: '山田太郎',
    post: { postId: 1, postTitle: '6月6日(山田太郎)' }
  },
  {
    userId: 2,
    userName: '田中花子',
    post: { postId: 3, postTitle: '6月6日(田中花子)' }
  }
]

-----------------------------------

すべてのユーザー、投稿とあわせて取得

[
  {
    "id": 1,
    "email": "yamada@example.com",
    "name": "山田太郎",
    "posts": [
      {
        "id": 2,
        "title": "6月7日(山田太郎)",
        "content": "Hello Universe",
        "authorId": 1
      },
      {
        "id": 1,
        "title": "6月6日(山田太郎)",
        "content": "Hello World",
        "authorId": 1
      }
    ]
  },
  {
    "id": 2,
    "email": "tanaka@example.com",
    "name": "田中花子",
    "posts": [
      {
        "id": 3,
        "title": "6月6日(田中花子)",
        "content": "Hello",
        "authorId": 2
      }
    ]
  }
]

-----------------------------------

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add code to select"

DrizzleのAPIを確認

ここでは、select.ts で利用した Drizle の API について説明します。

以下の URL に Drizzle が提供する API の利用方法がまとめられています。注意点として、データベースの種類に応じてインポートするパッケージ、呼び出す API が異なります。

PlanetScale の場合は MySQL を利用しているので、以下のドキュメントを参照します。

シナリオ:すべてのレコードを取得

すべてのレコードを取得する場合は以下のように記述します。SQL で記述すると SELECT * FROM users になります。SQL に慣れている人からすると直感的です。

  const selecAllUsers = await db.select().from(users);
  console.log("すべてのユーザーを取得");
  console.log(selecAllUsers);
出力結果
すべてのユーザーを抽出
[
  { id: 1, email: 'yamada@example.com', name: '山田太郎' },
  { id: 2, email: 'tanaka@example.com', name: '田中花子' }
]

シナリオ:取得するレコード件数を絞りたい

取得するレコードの件数を絞りたい場合は limit を利用します。下記では limit(1) で件数を 1 件に絞っています。SQL で記述すると SELECT * FROM users LIMIT 1 になります。こちらも、SQL に慣れている人からすると直感的です。

  const selec1Users = await db.select().from(users).limit(1);
  console.log("1件のユーザーを取得");
  console.log(selec1Users);
出力結果
1件のユーザーを取得
[ { id: 1, email: 'yamada@example.com', name: '山田太郎' } ]

シナリオ:取得するレコードを項目の値で絞りたい

取得するレコードを項目の値で絞りたい場合は、eq を利用します。ここでは、name山田太郎 と等しいデータに絞ります。SQL で記述すると SELECT * FROM users WHERE name = '山田太郎' になります。こちらも、SQL に慣れている人からすると直感的です。

  const selecYamadaTaroUsers = await db
    .select()
    .from(users)
    .where(eq(users.name, "山田太郎"));
  console.log("ユーザー名が山田太郎のユーザーを取得");
  console.log(selecYamadaTaroUsers);
出力結果
ユーザー名が山田太郎のユーザーを取得
[ { id: 1, email: 'yamada@example.com', name: '山田太郎' } ]

シナリオ:複数の条件で取得するレコードを項目の値で絞りたい

取得するレコードをの条件が複数ある場合は、andor を利用します。ここでは、name山田太郎 あるいは 田中花子 のレコードに絞ります。絞り込みするために、or を利用します。SQL で記述すると SELECT * FROM users WHERE name = '山田太郎' OR name = '田中花子' になります。こちらも、SQL に慣れている人からすると直感的です。

  const selecSuzukiHanakoOrYamadaTaroUsers = await db
    .select()
    .from(users)
    .where(or(eq(users.name, "山田太郎"), eq(users.name, "田中花子")));
  console.log("ユーザー名が山田太郎あるいは田中花子のユーザーを取得");
  console.log(selecSuzukiHanakoOrYamadaTaroUsers);
出力結果
ユーザー名が山田太郎あるいは田中花子のユーザーを取得
[
  { id: 1, email: 'yamada@example.com', name: '山田太郎' },
  { id: 2, email: 'tanaka@example.com', name: '田中花子' }
]

シナリオ:複数のテーブルを結合し結果を表示

最後に複数のテーブルを結合し、レコードを取得する場合は、LEFT JOIN を利用します。日本語だと、左外部結合と言います。ユーザーに紐づく投稿を author_id で結合します。SQL で記述すると SELECT * FROM user LEFT JOIN post ON user.id = post.author_id になります。

  const selecAllUsersAndPosts = await db
    .select({
      userId: users.id,
      userName: users.name,
      post: {
        postId: posts.id,
        postTitle: posts.title,
      },
    })
    .from(users)
    .leftJoin(posts, eq(users.id, posts.authorId))
    .orderBy(asc(users.id), desc(posts.id));
  console.log("すべてのユーザー、投稿とあわせて取得");
  console.log(selecAllUsersAndPosts);
出力結果
すべてのユーザー、投稿とあわせて取得
[
  {
    userId: 1,
    userName: '山田太郎',
    post: { postId: 2, postTitle: '6月7日(山田太郎)' }
  },
  {
    userId: 1,
    userName: '山田太郎',
    post: { postId: 1, postTitle: '6月6日(山田太郎)' }
  },
  {
    userId: 2,
    userName: '田中花子',
    post: { postId: 3, postTitle: '6月6日(田中花子)' }
  }
]

ただし、この出力結果だと開発するのには不向きです。以下のような入れ子構造で本来は欲しいです。

[
  {
    userId: 1,
    userName: '山田太郎',
    posts: [
      { postId: 2, postTitle: '6月7日(山田太郎)' },
      { postId: 1, postTitle: '6月6日(山田太郎)' }
    ]
  },
  {
    userId: 2,
    userName: '田中花子',
    posts: [
      { postId: 3, postTitle: '6月6日(田中花子)' }
    ]
  }
]

入れ子構造でデータを取得

入れ子構造でデータを取得するには、relational queryを使います。 Relational query は公式ドキュメントだと、記載方法が少なく、PlanetScale での設定方法がわかりにくいです。

事前にスキーマファイルで relations を設定しておくことで、データを入れ子構造で取得ができるようになります。

src/db/schema.ts
...
export const userRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
...

データを取得するには、query を使って記述します。詳しくは公式ドキュメントを参照してください。

  const queryAllUsersWithPosts = await db.query.users.findMany({
    with: {
      posts: true,
    },
  });
  console.log("すべてのユーザー、投稿とあわせて取得\n");
  console.log(JSON.stringify(queryAllUsersWithPosts, null, 2));

無事、入れ子構造で取得できました。

出力結果
すべてのユーザー、投稿とあわせて取得

[
  {
    "id": 1,
    "email": "yamada@example.com",
    "name": "山田太郎",
    "posts": [
      {
        "id": 2,
        "title": "6月7日(山田太郎)",
        "content": "Hello Universe",
        "authorId": 1
      },
      {
        "id": 1,
        "title": "6月6日(山田太郎)",
        "content": "Hello World",
        "authorId": 1
      }
    ]
  },
  {
    "id": 2,
    "email": "tanaka@example.com",
    "name": "田中花子",
    "posts": [
      {
        "id": 3,
        "title": "6月6日(田中花子)",
        "content": "Hello",
        "authorId": 2
      }
    ]
  }
]

Drizzleでデータをプログラムから投入

手動でデータ登録は面倒なので、プログラムからテスト用のデータを登録できるようにします。seed.ts は、postsusers からすべてのデータを削除し、サンプルデータを登録するプログラムです。プログラムでは処理が正しく実行された、コンソールに適宜データを出力しています。

$ touch src/db/seed.ts
src/db/seed.ts
import { db } from ".";
import { users, NewUser, posts, NewPost } from "./schema";

async function main() {
  // すべてのユーザーを削除
  //
  // 以下と同等のSQL
  // DELETE FROM user;
  const deleteAllUsers = await db.delete(users);
  console.log("すべてのユーザーを削除");
  console.log(deleteAllUsers);
  console.log("\n-----------------------------------\n");

  // すべてのユーザーを取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users;
  const selecAllDeletedUsers = await db.select().from(users);
  console.log("すべてのユーザーを取得");
  console.log(selecAllDeletedUsers);
  console.log("\n-----------------------------------\n");

  // 10件のユーザーを追加
  //
  // 以下と同等のSQL
  // INSERT INTO users (name, email) VALUES ('山田太郎', 'yamada@example.com');
  // INSERT INTO users (name, email) VALUES ('田中花子', 'tanaka@example.com');
  const newUsers: NewUser[] = [
    {
      name: "山田太郎",
      email: "yamada@example.com",
    },
    {
      name: "田中花子",
      email: "tanaka@example.com",
    },
  ];
  const insertUsers = await db.insert(users).values(newUsers);
  console.log("ユーザーを追加");
  console.log(insertUsers);
  console.log("\n-----------------------------------\n");

  // すべてのユーザーを取得
  //
  // 以下と同等のSQL
  // SELECT * FROM users;
  const selecAllUsers = await db.select().from(users);
  console.log("すべてのユーザーを取得");
  console.log(selecAllUsers);
  console.log("\n-----------------------------------\n");

  // すべての投稿を取得
  //
  // 以下と同等のSQL
  // DELETE FROM posts;
  const deleteAllPosts = await db.delete(posts);
  console.log("すべての投稿を取得");
  console.log(deleteAllPosts);
  console.log("\n-----------------------------------\n");

  // すべてのユーザーを抽出
  //
  // 以下と同等のSQL
  // SELECT * FROM posts;
  const selecAllDeletedPosts = await db.select().from(posts);
  console.log("すべての投稿を取得");
  console.log(selecAllDeletedPosts);
  console.log("\n-----------------------------------\n");

  // 10件のユーザーを追加
  //
  // 以下と同等のSQL
  // INSERT INTO posts (title, content, published, author_id) VALUES ('6月6日(山田太郎)', 'Hello World', true, 1);
  // INSERT INTO posts (title, content, published, author_id) VALUES ('6月7日(山田太郎)', 'Hello Universe', true, 1);
  // INSERT INTO posts (title, content, published, author_id) VALUES ('6月6日(田中花子)', 'Hello', true, 2);
  const newPosts: NewPost[] = [
    {
      title: "6月6日(山田太郎)",
      content: "Hello World",
      authorId: selecAllUsers[0].id,
    },
    {
      title: "6月7日(山田太郎)",
      content: "Hello Universe",
      authorId: selecAllUsers[0].id,
    },
    {
      title: "6月7日(田中花子)",
      content: "Hello",
      authorId: selecAllUsers[1].id,
    },
  ];
  const insertPosts = await db.insert(posts).values(newPosts);
  console.log("3件の投稿を追加");
  console.log(insertPosts);
  console.log("\n-----------------------------------\n");

  // すべてのユーザーを抽出
  //
  // 以下と同等のSQL
  // SELECT * FROM posts;
  const selecAllPosts = await db.select().from(posts);
  console.log("すべての投稿を取得");
  console.log(selecAllPosts);
  console.log("\n-----------------------------------\n");
}

main();

package.json にスクリプトを追加します。

package.json
{
  "scripts": {
+    "db:seed": "pnpm with-env node -r esbuild-register src/db/seed.ts",
  }
}

スクリプトを実行します。

$ pnpm db:seed

> nextjs-planetscale-drizzle@0.1.0 db:seed /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/seed.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/seed.ts"

すべてのユーザーを削除
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 2,
  insertId: '0',
  size: 0,
  statement: 'delete from `users`',
  time: 12.206456
}

-----------------------------------

すべてのユーザーを取得
[]

-----------------------------------

ユーザーを追加
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 2,
  insertId: '3',
  size: 0,
  statement: "insert into `users` (`id`, `email`, `name`) values (default, 'yamada@example.com', '山田太郎'), (default, 'tanaka@example.com', '田中花子')",
  time: 10.069541
}

-----------------------------------

すべてのユーザーを取得
[
  { id: 3, email: 'yamada@example.com', name: '山田太郎' },
  { id: 4, email: 'tanaka@example.com', name: '田中花子' }
]

-----------------------------------

すべての投稿を取得
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 3,
  insertId: '0',
  size: 0,
  statement: 'delete from `posts`',
  time: 12.903208
}

-----------------------------------

すべての投稿を取得
[]

-----------------------------------

3件の投稿を追加
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 3,
  insertId: '4',
  size: 0,
  statement: "insert into `posts` (`id`, `title`, `content`, `author_id`) values (default, '6月6日(山田太郎)', 'Hello World', 3), (default, '6月7日(山田太郎)', 'Hello Universe', 3), (default, '6月7日(田中花子)', 'Hello', 4)",
  time: 10.501185999999999
}

-----------------------------------

すべての投稿を取得
[
  { id: 4, title: '6月6日(山田太郎)', content: 'Hello World', authorId: 3 },
  {
    id: 5,
    title: '6月7日(山田太郎)',
    content: 'Hello Universe',
    authorId: 3
  },
  { id: 6, title: '6月7日(田中花子)', content: 'Hello', authorId: 4 }
]

-----------------------------------

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "add code to seed"

Next.jsのページを作成

PlanetScale のデータベースに Drizzle を通してアクセスし、取得したデータを表示するページを作成します。ページはエッジで動作するように設定します。 page.tsx は以下のように修正します。

src/app/page.tsx
import { db } from "@/db";

export const runtime = "edge";

export default async function Home() {
  const users = await db.query.users.findMany({
    with: {
      posts: true,
    },
  });

  return (
    <>
      <div className="grid grid-cols-2 gap-x-5 gap-y-5 my-5 mx-auto max-w-2xl">
        {(await users).map((user, index) => (
          <div key={user.id} className="bg-blue-100 rounded-md p-5">
            <h1 className="text-lg font-bold">{user.name}</h1>
            <ul>
              <li>{user.email}</li>
            </ul>
            <div className="bg-red-100 mt-5 p-5 rounded-md">
              <h1 className="text-sm font-bold">記事投稿</h1>
              <ul>
                <li>
                  {user.posts.map((post) => (
                    <div key={post.id}>
                      <h1 className="text-sm font-bold mt-5">{post.title}</h1>
                      <ul>
                        <li>{post.content}</li>
                      </ul>
                    </div>
                  ))}
                </li>
              </ul>
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

ローカル環境で実行します。

$ pnpm dev

ページでデータが表示されました。

コミットします。

$ git checkout main
$ pnpm build
$ git add .
$ git commit -m "modify page.tsx to display database contents"

Vercelに変更内容をデプロイ

これまでの内容を Vercel へデプロイするために、GitHub のリポジトリに変更点をプッシュします。GitHub のリポジトリに変更点をプッシュする前に、Vercel の環境変数に開発環境の DATABASE_URL を追加します。

  1. Settings をクリック
  2. Environment Variables をクリック
  3. 環境変数の名前に DATABASE_URL を入力
  4. 環境変数の値に接続先の情報を入力
  5. Production 以外は選択を外す
  6. 「Save」をクリックし保存

無事保存できました。

続いて、これまでの変更をリポジトリにプッシュします。

$ pnpm build
$ git push origin main

Vercel に新しいデプロイが作成されました。

デプロイが完了しました。

「Visit」をクリックします。

デプロイしたページが表示されました。production 環境で実行されていることがわかります。

ここまでで、PlanetScale を使いデータベースを作成し、データを取得するアプリケーションを Next.js を利用し構築し、Vercel 上に構築できました。

チェックポイント

ここまでが一区切りです。以降、開発環境として商用環境と開発環境を分けていきます。そして、開発環境でデータベースのスキーマを変更し、商用環境にマージしていきます。

PlanetScaleのブランチ機能について

PlanetScale ではブランチ機能が実装されています。これは PlanetScale ならではの強みです。

ブランチ機能を利用すると、商用のブランチ/データベースから枝を生やし開発用のブランチ/データベースを作ることができます。開発用のブランチで、新たな機能を実装・テストし、問題がなければ商用のブランチにマージできます。これにより、従来の商用のデータベースと開発用のデータベースを2つ管理しなければならなかった所を、1 つにまとめることができます。PlanetScale では Git のブランチと同じようにデータベースを管理できるのが特徴です。

https://planetscale.com/docs/concepts/branching

PlanetScaleの開発用ブランチを商用ブランチに昇格

PlanetScale の使っている現状のデータベースは開発用のため、商用に昇格させます。

PlanetScale のダッシュボードに戻り、「Branches」をクリックします。

現状はデフォルトで作成された main ブランチしかありません。このブランチを商用に昇格させます。「main」をクリックします。

「Promote to production」をクリックし、「main」ブランチを商用に昇格させます。

main ブランチが商用に昇格しました。画面が更新されない場合は、「command」+「r」で画面をリフレッシュしてください。

PlanetScaleで商用ブランチのSafe Migrationsを有効化

PlanetScale では本番環境のブランチを保護するために、Safe Migrations という機能を提供しています。Safe Migrations とは、Production への意図しない Schema の変更を防ぐ機能です。

https://planetscale.com/docs/concepts/safe-migrations

商用ブランチを保護するために、Safe Migrations をオンにします。

「Enable Safe Migrations」をクリックし、Safe Migrations を有効化します。

無事、有効化できました。画面が更新されない場合は、「command」+「r」で画面をリフレッシュしてください。

PlanetScaleで開発用ブランチを作成

ここでは、PlanetScale で開発用のデータベースのブランチを作成します。

「New branch」をクリックし、開発用ブランチを作成します。

開発用ブランチの「Name」に任意の名前を入力し、「Create branch」をクリックし、開発用ブランチを作成します。

開発用ブランチが作成されました。

「Branches」をクリックし、ブランチ一覧を表示します。

商用ブランチと開発用ブランチが表示されています。商用データベースと開発用野データベースが分けて管理されていることが分かります。

Gitで開発用ブランチを作成

ローカルの開発環境として開発用ブランチを作成します。

$ git checkout -b dev

今後は開発用ブランチで作業をし、プルリクで main にマージしていきます。

開発用データベースへの接続先情報を取得

作成した開発用ブランチ(データベース)にアクセスするため、接続情報を取得します。

開発用ブランチをクリックします。

「connect」をクリックします。

「New password」をクリックし、認証情報を作成します。

「Branch」をクリックし、開発用ブランチを選択します。「Create password」をクリックし、認証情報を作成します。

「Connection with」は前回同様に「Prisma」を選択します。DATABASE_URL をコピーします。

開発用データベースの接続先情報を開発環境に設定

ローカルの開発環境に、開発用データベースの接続先情報を設定します。

.env に開発用のデータベースの情報をペーストします。今後は、ローカルの開発では、開発用のデータベースを使います。

DATABASE_URL='mysql://7l2dsfdsafsafsafao5ggbzp:pscale_pw_DNadafAefrKdkfL3kf9aldfak9OOROjz272rEBC9hj@aws.connect.psdb.cloud/nextjs-planetscale-drizzle?sslaccept=strict'

開発用データベースのデータを確認

開発用データベースにデータが入っているか、作成した db:select で確認します。データベースにはデータが入っていないため、空になります。

コマンドを実行し、データベースの中身を表示します。

$ pnpm db:select

> nextjs-planetscale-drizzle@0.1.0 db:select /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/select.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/select.ts"

すべてのユーザーを取得
[]

-----------------------------------

すべての投稿を取得
[]

-----------------------------------

1件のユーザーを取得
[]

-----------------------------------

ユーザー名が山田太郎のユーザーを取得
[]

-----------------------------------

ユーザー名が山田太郎あるいは田中花子のユーザーを取得
[]

-----------------------------------

すべてのユーザー、投稿とあわせて取得
[]

-----------------------------------

すべてのユーザー、投稿とあわせて取得

[]

-----------------------------------

開発用データベースにデータを投入

開発用データベースは現状からのため、サンプルデータを投入します。

コマンドを実行します。

$ pnpm db:seed

> nextjs-planetscale-drizzle@0.1.0 db:seed /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/seed.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/seed.ts"

すべてのユーザーを削除
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 0,
  insertId: '0',
  size: 0,
  statement: 'delete from `users`',
  time: 2.756868
}

-----------------------------------

すべてのユーザーを取得
[]

-----------------------------------

ユーザーを追加
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 2,
  insertId: '1',
  size: 0,
  statement: "insert into `users` (`id`, `email`, `name`) values (default, 'yamada@example.com', '山田太郎'), (default, 'tanaka@example.com', '田中花子')",
  time: 15.605739999999999
}

-----------------------------------

すべてのユーザーを取得
[
  { id: 1, email: 'yamada@example.com', name: '山田太郎' },
  { id: 2, email: 'tanaka@example.com', name: '田中花子' }
]

-----------------------------------

すべての投稿を取得
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 0,
  insertId: '0',
  size: 0,
  statement: 'delete from `posts`',
  time: 2.946462
}

-----------------------------------

すべての投稿を取得
[]

-----------------------------------

3件の投稿を追加
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 3,
  insertId: '1',
  size: 0,
  statement: "insert into `posts` (`id`, `title`, `content`, `author_id`) values (default, '6月6日(山田太郎)', 'Hello World', 1), (default, '6月7日(山田太郎)', 'Hello Universe', 1), (default, '6月7日(田中花子)', 'Hello', 2)",
  time: 8.941819
}

-----------------------------------

すべての投稿を取得
[
  { id: 1, title: '6月6日(山田太郎)', content: 'Hello World', authorId: 1 },
  {
    id: 2,
    title: '6月7日(山田太郎)',
    content: 'Hello Universe',
    authorId: 1
  },
  { id: 3, title: '6月7日(田中花子)', content: 'Hello', authorId: 2 }
]

-----------------------------------

データが表示されるかページで確認

開発環境のデータベースにデータの登録ができたので、ローカルのページでデータが表示されるか確認します。

$ pnpm dev

無事表示されました。

Drizzleで開発用データベースのスキーマを変更

開発用のデータベースのスキーマを変更し、どのようの商用環境に反映していくか確認していきます。ここでは、スキーマを変更します。

posts テーブルに投稿日時を表す publishedAt を追加します。

src/db/schema.ts
import {
  mysqlTable,
  serial,
  text,
  int,
  index,
+  timestamp,
} from "drizzle-orm/mysql-core";
import { InferModel, relations } from "drizzle-orm";

export const users = mysqlTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull(),
  name: text("name").notNull(),
});

export const posts = mysqlTable(
  "posts",
  {
    id: serial("id").primaryKey(),
    title: text("title").notNull(),
    content: text("content").notNull(),
+    publishedAt: timestamp("published_at").defaultNow().notNull(),
    authorId: int("author_id").notNull(),
  },
  (table) => ({
    authorIdIndex: index("authorId_idx").on(table.authorId),
  })
);

export const userRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

export type User = InferModel<typeof users>;
export type NewUser = InferModel<typeof users, "insert">;
export type Post = InferModel<typeof posts>;
export type NewPost = InferModel<typeof posts, "insert">;

コミットします。

$ git checkout dev
$ pnpm build
$ git add .
$ git commit -m "add published_at to schema file"

Drizzleでマイグレーションファイルの作成

マイグレーションに必要なファイルを作成します。

$ pnpm db:generate

> nextjs-planetscale-drizzle@0.1.0 db:generate /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm drizzle-kit generate:mysql --config drizzle.config.ts

drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

Reading config file '/Users/hayato94087/Private/nextjs-planetscale-drizzle/drizzle.config.ts'
Reading schema files:
/Users/hayato94087/Private/nextjs-planetscale-drizzle/src/db/schema.ts

2 tables
posts 5 columns 1 indexes 0 fks
users 3 columns 0 indexes 0 fks

[] Your SQL migration file ➜ src/db/migrations/0001_fair_malice.sql 🚀

作成された SQL ファイルを確認します。published_at カラムが追加されています。

src/db/migrations/0001_fair_malice.sql
ALTER TABLE `posts` ADD `published_at` timestamp DEFAULT (now()) NOT NULL;

コミットします。

$ git checkout dev
$ pnpm build
$ git add .
$ git commit -m "create migration files"

Drizzleで過去のマイグレーションファイルの削除

次に pnpm db:push をしたいところですが、実施するとエラーになります。まずは、過去のマイグレーションファイルを削除します。これまで作成したマイグレイーションファイルを drizzle-kit drop で削除します。

コマンドを作成します。

package.json
{
  "scripts": {
+    "db:drop": "drizzle-kit drop --config drizzle.config.ts",
  },
}

ドロップコマンドを実行します。実行するとどのファイルをドロップするか聞かれます。一個前のマイグレーションファイルを選択してドロップします。

$ pnpm db:drop

> nextjs-planetscale-drizzle@0.1.0 db:drop /Users/hayato94087/Private/nextjs-planetscale-drizzle
> drizzle-kit drop --config drizzle.config.ts

drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

Reading config file '/Users/hayato94087/Private/nextjs-planetscale-drizzle/drizzle.config.ts'
Please select migration to drop:
❯ 0000_magical_slapstick
  0001_fair_malice

最終的なログです。

> nextjs-planetscale-drizzle@0.1.0 db:drop /Users/hayato94087/Private/nextjs-planetscale-drizzle
> drizzle-kit drop --config drizzle.config.ts

drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

Reading config file '/Users/hayato94087/Private/nextjs-planetscale-drizzle/drizzle.config.ts'

[] 0000_magical_slapstick migration successfully dropped

マイグレーションフォルダーを確認すると、ファイルが削除されていることがわかります。

$ tree src/db/migrations

src/db/migrations
├── 0001_fair_malice.sql
└── meta
    ├── 0001_snapshot.json
    └── _journal.json

2 directories, 3 files

コミットします。

$ git checkout dev
$ pnpm build
$ git add .
$ git commit -m "add script to drop migration files and perform drop previous migration files"

PlanetScaleの開発用ブランチに変更をプッシュ

開発用 DB に変更をプッシュします。

$ pnpm db:push

> nextjs-planetscale-drizzle@0.1.0 db:push /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/migrate.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/migrate.ts"

⏳ Running migrations...
✅ Migrations completed in 381ms

変更されたか PlanetScale のダッシュボードで確認します。

「dev」をクリックして開発用ブランチを確認します。

変更点が表示されていることが確認できます。

Drizzleでデータを登録するコードを修正

スキーマの変更にあわせて Seed を投入するプログラムを修正します。

seed.ts を修正します。不要な場所は表示を省略しています。

src/db/seed.ts
async function main() {
  const newPosts: NewPost[] = [
    {
      title: "6月6日(山田太郎)",
      content: "Hello World",
+      publishedAt: new Date(),
      authorId: selecAllUsers[0].id,
    },
    {
      title: "6月7日(山田太郎)",
      content: "Hello Universe",
+      publishedAt: new Date(),
      authorId: selecAllUsers[0].id,
    },
    {
      title: "6月7日(田中花子)",
      content: "Hello",
+      publishedAt: new Date(),
      authorId: selecAllUsers[1].id,
    },
  ];
}

データ投入します。

$ pnpm db:seed

> nextjs-planetscale-drizzle@0.1.0 db:seed /Users/hayato94087/Private/nextjs-planetscale-drizzle
> pnpm with-env node -r esbuild-register src/db/seed.ts


> nextjs-planetscale-drizzle@0.1.0 with-env /Users/hayato94087/Private/nextjs-planetscale-drizzle
> dotenv -e .env -- "node" "-r" "esbuild-register" "src/db/seed.ts"

すべてのユーザーを削除
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 2,
  insertId: '0',
  size: 0,
  statement: 'delete from `users`',
  time: 7.412097
}

-----------------------------------

すべてのユーザーを取得
[]

-----------------------------------

ユーザーを追加
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 2,
  insertId: '3',
  size: 0,
  statement: "insert into `users` (`id`, `email`, `name`) values (default, 'yamada@example.com', '山田太郎'), (default, 'tanaka@example.com', '田中花子')",
  time: 8.237474
}

-----------------------------------

すべてのユーザーを取得
[
  { id: 3, email: 'yamada@example.com', name: '山田太郎' },
  { id: 4, email: 'tanaka@example.com', name: '田中花子' }
]

-----------------------------------

すべての投稿を取得
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 3,
  insertId: '0',
  size: 0,
  statement: 'delete from `posts`',
  time: 7.561539
}

-----------------------------------

すべての投稿を取得
[]

-----------------------------------

3件の投稿を追加
{
  headers: [],
  types: {},
  fields: [],
  rows: [],
  rowsAffected: 3,
  insertId: '4',
  size: 0,
  statement: "insert into `posts` (`id`, `title`, `content`, `published_at`, `author_id`) values (default, '6月6日(山田太郎)', 'Hello World', '2023-06-14 09:19:27', 3), (default, '6月7日(山田太郎)', 'Hello Universe', '2023-06-14 09:19:27', 3), (default, '6月7日(田中花子)', 'Hello', '2023-06-14 09:19:27', 4)",
  time: 9.827587000000001
}

-----------------------------------

すべての投稿を取得
[
  {
    id: 4,
    title: '6月6日(山田太郎)',
    content: 'Hello World',
    publishedAt: 2023-06-14T09:19:27.000Z,
    authorId: 3
  },
  {
    id: 5,
    title: '6月7日(山田太郎)',
    content: 'Hello Universe',
    publishedAt: 2023-06-14T09:19:27.000Z,
    authorId: 3
  },
  {
    id: 6,
    title: '6月7日(田中花子)',
    content: 'Hello',
    publishedAt: 2023-06-14T09:19:27.000Z,
    authorId: 4
  }
]

-----------------------------------

コミットします。

$ git checkout dev
$ pnpm build
$ git add .
$ git commit -m "add publishedAt column to seed"

Next.jsのページを修正

スキーマの変更にあわせてページ(page.tsx)を修正します。

page.tsx を修正します。不要な場所は表示を省略しています。

src/app/page.tsx
export default async function Home() {
  return (
    <>
      ...
                    <div key={post.id}>
                      <h1 className="text-sm font-bold mt-5">{post.title}</h1>
                      <ul>
                        <li>{post.content}</li>
+                       <li>{post.publishedAt.toLocaleDateString()}</li>
                      </ul>
                    </div>
      ...
    </>
  );
}

ローカルで確認します。

$ pnpm dev

日付が追加されていることを確認できたので、無事スキーマの変更が反映されています。

コミットします。

$ git checkout dev
$ pnpm build
$ git add .
$ git commit -m "add published_at to page.tsx"

開発環境をVercelにデプロイ

開発用ブランチをプッシュします。

$ git push origin dev

開発用ブランチの内容が Vercel にデプロイされました。

「Visit」をクリックし、デプロイされたページを確認します。

するとエラーになっています。これは、開発用 DB の環境変数を登録していないからです。

開発用 DB の DATABASE_URL を環境変数として登録します。

  1. Settings をクリック
  2. Environment Variables をクリック
  3. 環境変数の名前に DATABASE_URL を入力
  4. 環境変数の値に接続先の情報を入力
  5. Preview 以外は選択を外し、ブランチは dev を選択
  6. 「Save」をクリックし保存

登録できました。

過去のデプロイを再度デプロイすることで、先程登録した環境変数を反映した環境でデプロイできます。

  1. Deployments をクリックし、過去のデプロイ一覧を表示
  2. 直近に失敗したデプロイの詳細ボタンをクリックしメニューを表示
  3. 「Redeploy」をクリックし、再デプロイ

Redeploy をクリックします。

デプロイが完了しました。Visit をクリックします。

スキーマに追加した日付が表示されていることをが確認できました。

PlanetScaleで開発用ブランチを商用ブランチにマージ

PlanetScale の開発用ブランチを商用ブランチにマージします。

開発用ブランチの「dev」をクリックします。

「Create deploy request」をクリックし、商用ブランチへ統合するための「デプロイリクエスト」を作成します。

これがデプロイリクエストの画面です。統合可能な場合は、「Deploy changes」が表示されます。逆に統合できない場合は、統合できない理由が表示されます。「Deply changes」をクリックします。

デプロイが完了しました。

PlanetScaleのデプロイリクエスト一覧

なお、デプロイリクエストの一覧を確認できます。「Deploy requests」をクリックします。

以下が、デプロイリクエストの一覧です。デプロイリクエストの一覧には、過去のすべてのデプロイリクエストが表示されます。

Vercelの商用環境にデプロイ

Vercel の商用環境にデプロイするため、GitHub リポジトリの main ブランチに dev をマージします。

gitHub の画面で、「Compare & pull request」をクリックします。

「Create pull request」をクリックし、プルリクエストを生成します。

「Merge pull request」をクリックします。

「Confirm merge」をクリックしマージさせます。

マージが完了しました。

Vercel の画面を確認すると、デプロイされています。

Visit をクリックし商用環境にアクセスします。

商用環境のサイトで、無事日付が表示されているため、無事変更が反映されていることが分かります。

PlanetScale で開発用ブランチから商用ブランチにスキーマはマージされましたが、データはマージされていません。なので、画面に表示されている日付が何の値が入っているかが気になります。

PlanetScaleで商用環境のデータをコンソールから確認

商用ブランチに追加された published_at の値を PlanetScale のコンソールから確認します。

PlanetScale のコンソールにアクセスします。商用環境は保護されているため、商用環境からアクセスできません。が、解除できます。「database setting」をクリックします。

設定画面で、商用ブランチへのアクセスをコンソールから許可させます。

  1. 「Allow web console access to production branches」にチェックを入れる
  2. 「Save database settings」をクリックし、設定を保存

商用ブランチ(main)のコンソールにアクセスできます。「Connect」をクリックします。

select * from posts の出力結果の published_at の値から推察できます。開発用ブランチから商用ブランチにマージされた際、デフォルトとしてそのタイミングの日時が入っているようです。

まとめ

かなり長くなりましたが、この記事では以下を行いました。

  • PlanetScale でアカウント作成/データベース作成
  • PlanetScale のデータベースに Next.js からアクセスしデータを操作、ページにデータを表示
  • Vercel にデプロイし、エッジで PlanetScale のデータを取得し、ページにデータを表示

さらに以下も行いました。

  • PlanetScale で開発用ブランチと商用ブランチを分ける
  • PlanetScale で開発用ブランチに修正を加えて商用ブランチに統合

Discussion

melodyclue_routermelodyclue_router

実際の運用では、この方法だと、アプリを商用環境にデプロイするのと、DBを商用ブランチにマージするので時間差がでてきてしまい、アプリがクラッシュする可能性があると思うのですが、このへん一般的にどうなんでしょうか?