デザインについての学習メモブログ

Next.js入門 #9 【実践編】 データベース — PrismaとSupabaseで永続化

記事内に広告が含まれています。

Next.js入門 #9 【実践編】 データベース — PrismaとSupabaseで永続化

入門編では、記事データを配列やオブジェクトとしてメモリ上に持ち、Route HandlerやServer Actionsで読み書きしてきました。

ですが、この方法にはひとつ大きな問題があります。

サーバーを再起動するとデータが消えてしまうのです。

今回は、この問題を解決するために本物のデータベースを導入します。

データベースにはSupabase(PostgreSQLのホスティングサービス)を使い、Next.jsからのアクセスにはPrismaというORM(Object-Relational Mapping)を使います。

この記事は#56で扱ったRoute HandlerとServer Actionsの知識を前提にしています。

まだの方は先にそちらをご確認ください。

この記事のゴール

  • Supabaseでデータベースを用意できるようになる
  • Prismaでスキーマを定義し、マイグレーションを実行できるようになる
  • Prisma Clientを使ってNext.jsアプリからDBを読み書きできるようになる
  • 今まで配列で管理していた記事データを、本物のDBに置き換える

なぜ配列ではダメなのか

これまでのコードは、次のようなイメージで記事データを管理していたと思います。

TypeScript
// これまでのやり方(メモリ上の配列)
let posts = [
  { id: 1, title: "はじめてのNext.js", content: "..." },
  { id: 2, title: "App Routerとは", content: "..." },
];

この方法には次のような問題があります。

  • サーバーが再起動すると消える:開発中にホットリロードが走るだけでもデータがリセットされます
  • 複数のサーバーインスタンスで共有できない:本番環境ではサーバーが複数台になることがあり、配列はプロセスごとに独立してしまいます
  • 検索・並び替え・ページネーションが自前実装になる:本来DBが得意とする処理を全部自分で書く必要があります

これらを解決するのがデータベースです。

Supabaseでデータベースを用意する

Supabaseは、PostgreSQLをクラウド上で簡単に使えるようにしたサービスです。

無料プランでも十分に開発を進められます。

1. プロジェクトを作成する

Supabaseの公式サイトにアクセスし、GitHubアカウントなどでサインアップします。

ダッシュボードから「New Project」を選び、プロジェクト名とデータベースのパスワードを設定します。

パスワードは後で接続文字列に使うので、忘れないようメモしておいてください。

2. 接続文字列を取得する

プロジェクトが作成できたら、Project SettingsDatabase から接続文字列(Direct Connection String)をコピーします。

Prismaで使う場合は、URI形式のものを使います。

Bash
postgresql://postgres:[YOUR-PASSWORD]@db.xxxxxxxxxxxx.supabase.co:5432/postgres

[YOUR-PASSWORD]の部分を、先ほど設定したパスワードに置き換えてください。

Prismaをセットアップする

1. インストール

Bash
npm install prisma --save-dev
npm install @prisma/client @prisma/adapter-pg pg
npm install -D @types/pg

⚠️注意:npm audit fix --forceは実行しない

インストール時に脆弱性の警告が表示されることがありますが、npm audit fix --forceは絶対に実行しないでください。

このコマンドはpackage.jsonの指定を無視して、npmが「脆弱性がない」と判断したバージョンに依存パッケージを強制的に書き換えてしまいます。

Prismaのようにメジャーバージョンで破壊的変更が多いパッケージでは、これが原因でprisma(CLI)だけが意図せず古いバージョンに戻され、@prisma/clientとバージョンが噛み合わなくなる、という事故が起きやすいです。

脆弱性が気になる場合は、まず--forceなしのnpm auditで内容を確認し、Prisma関連が対象に含まれていないかを見てから個別に判断してください。

インストール後、prisma(CLI)と@prisma/clientのバージョンが揃っているか確認しておきましょう。

Bash
npx prisma --version

prisma@prisma/client同じメジャーバージョン(7系)になっていれば問題ありません。

もしズレていた場合は、両方を明示的に同じバージョンで入れ直します。

Bash
npm install prisma@7.8.0 @prisma/client@7.8.0 --save-exact

2. 初期化

Bash
npx prisma init

このコマンドを実行すると、プロジェクトのルートに次の構成(prismaフォルダと.env、prisma.config.tsなど)が追加生成されます。

Bash
prisma/
  schema.prisma
prisma.config.ts
.env

.envファイルに、先ほどコピーしたSupabaseの接続文字列を設定します。

Bash
# .env
DATABASE_URL="postgresql://postgres:your-password@db.xxxxxxxxxxxx.supabase.co:5432/postgres"

.env.gitignoreに含まれていることを必ず確認してください。
接続情報をGitHubに公開してしまうと、第三者にデータベースを操作されるリスクがあります。

3. prisma.config.tsを設定する

Prisma 7では、prisma migrateprisma studioといったCLIコマンドが使う接続先を、schema.prismaではなくprisma.config.tsから読み込みます。

npx prisma initで生成されたファイルを、以下のように編集します。

Bash
// prisma.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

import "dotenv/config"を忘れると、.envの値が読み込まれずCLIコマンドがエラーになるので注意してください。

スキーマを定義する

prisma/schema.prismaを開き、記事(Post)のモデルを定義します。

Prisma
// prisma/schema.prisma

generator client {
  provider = "prisma-client"
  output   = "../app/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

⚠️ datasourceブロックにurl書いていない点に注目してください。

Prisma 7以降、接続文字列は先ほど作成したprisma.config.ts側で管理する設計に変わったため、schema.prismaには書きません。

ここにurl = env("DATABASE_URL")を残したままにすると、The datasource property 'url' is no longer supported in schema files.というエラーになります。

また、generatorブロックもprovider = "prisma-client"outputの指定に変わっています。

これにより、生成されたPrisma Clientのコードがnode_modulesではなくapp/generated/prisma以下に出力されるようになります。

各フィールドの意味は次のとおりです。

フィールド説明
id主キー。autoincrement()で自動採番されます
title / content記事のタイトルと本文
published公開状態のフラグ。デフォルトはfalse(下書き)
createdAt作成日時。default(now())で自動設定
updatedAt更新日時。@updatedAtで更新のたびに自動更新

マイグレーションを実行する

スキーマを定義したら、実際のデータベースにテーブルを作成します。

Bash
npx prisma migrate dev --name init

このコマンドは以下を行います。

  1. schema.prismaの内容に基づいてSQLマイグレーションファイルを生成
  2. Supabase上のデータベースに反映
  3. Prisma Clientを自動生成

実行後、prisma/migrationsフォルダにSQLファイルが生成されているはずです。

中身を覗いてみると、実際に発行されたSQL文を確認できます。

SQL
-- prisma/migrations/xxxxxxx_init/migration.sql
CREATE TABLE "Post" (
    "id" SERIAL NOT NULL,
    "title" TEXT NOT NULL,
    "content" TEXT NOT NULL,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);

Prisma Clientを使う準備

Prisma 7では、内部のクエリエンジンがスリム化された影響で、new PrismaClient()を引数なしで呼び出すことができなくなりました。

必ずドライバーアダプターを渡す必要があります。

今回はPostgreSQL用の@prisma/adapter-pgを使い、pgパッケージのコネクションプールと組み合わせます。

また、Next.jsの開発モードではホットリロードのたびにPrisma Clientのインスタンスが再生成され、DB接続がすぐに枯渇してしまうという問題もあります。

これを避けるため、シングルトンパターンでインスタンスを管理するのが定番です。

TypeScript
// lib/prisma.ts
import { PrismaClient } from "@/app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
  pool: Pool | undefined;
};

const pool =
  globalForPrisma.pool ?? new Pool({ connectionString: process.env.DATABASE_URL });

const adapter = new PrismaPg(pool);

export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
  globalForPrisma.pool = pool;
}

以降は、DBを操作したいファイルでこのprismaをimportして使います。

TypeScript
import { prisma } from "@/lib/prisma";

次のセクションで具体的な使用例を記載しています。

配列をPrismaに置き換える

それでは、実際にこれまでのServer Actionsを本物のDB操作に書き換えていきましょう。

記事一覧を取得する

コードの可読性を上げるためスタイルの指定は省略しています。

見やすいようにスタイルを調整してください。

TSX
// app/posts/page.tsx
import { prisma } from "@/lib/prisma";

export default async function PostsPage() {
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: "desc" },
  });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  );
}

Server Componentの中で直接prisma.post.findMany()を呼び出しています。

APIを経由せず、サーバー上で直接DBにアクセスできるのがApp Routerの大きな利点です。

記事を作成する(Server Actions)

TypeScript
// app/posts/actions.ts
"use server";

import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;

  if (!title || !content) {
    throw new Error("タイトルと本文は必須です");
  }

  await prisma.post.create({
    data: { title, content },
  });

  revalidatePath("/posts");
}
TSX
// app/posts/new/page.tsx
import { createPost } from "../actions";

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input type="text" name="title" placeholder="タイトル" required />
      <textarea name="content" placeholder="本文" required />
      <button type="submit">投稿する</button>
    </form>
  );
}

配列をpushしていた処理が、prisma.post.create()に置き換わっただけです。

データの永続化を意識する必要がなくなり、コード自体はむしろシンプルになっています。

記事を更新・削除する

typescript

TypeScript
// app/posts/actions.ts(続き)

export async function updatePost(id: number, formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;

  await prisma.post.update({
    where: { id },
    data: { title, content },
  });

  revalidatePath("/posts");
}

export async function deletePost(id: number) {
  await prisma.post.delete({
    where: { id },
  });

  revalidatePath("/posts");
}

findMany / create / update / deleteという一貫したAPIで、CRUDのすべてを表現できるのがPrismaの特長です。

Prisma Studioでデータを確認する

Prisma Studioは、必須ではないですが、コードを書かなくても、GUIでデータベースの中身を確認・編集できるツールが用意されています。

Bash
npx prisma studio

ブラウザが自動的に開き、テーブルの中身を一覧・編集できる画面が表示されます。

デバッグ時にとても重宝するので、ぜひ使ってみてください。

つまずきやすいポイント

Prisma 7はバージョンが新しく、既存の情報が古いままになっていることも多いため、セットアップ時によく遭遇するエラーをまとめておきます。

「モジュール “@prisma/client” にエクスポートされたメンバー ‘PrismaClient’ がありません」

generatorブロックにoutputを指定した場合、PrismaClient@prisma/clientパッケージ本体からはエクスポートされなくなります。

生成先のパス(@/app/generated/prisma/clientなど)から直接importしているか確認してください。

「モジュール ‘@/app/generated/prisma/client’ またはそれに対応する型宣言が見つかりません」

多くの場合、npx prisma generate(またはnpx prisma migrate dev)をまだ実行していないことが原因です。

生成先のディレクトリは、これらのコマンドを実行して初めて作られます。

それでも解決しない場合は、tsconfig.jsonpaths@/がどこを指しているか(srcディレクトリ構成かどうか)と、schema.prismaoutputパスの相対位置が正しく噛み合っているかを確認してください。

「モジュール ‘@prisma/adapter-pg’ / ‘pg’ またはそれに対応する型宣言が見つかりません」

単純に、@prisma/adapter-pgpg@types/pgのいずれかがインストールされていないことが原因です。

npm audit fix --forceを実行した場合、依存関係の解決の過程でこれらが巻き込まれて消えてしまうこともあるため、package.jsonを見て入っているか確認してください。

まとめ

この記事では、以下を行いました。

  • Supabaseでデータベースをセットアップ
  • Prismaでスキーマを定義し、マイグレーションを実行
  • Server Components / Server Actionsから直接Prisma Clientを呼び出し、CRUDを実装

これで、記事データが本当の意味で「永続化」されるようになりました。サーバーを再起動しても、デプロイし直しても、データは消えません。

次回の#10 認証を実装する — Auth.jsでGoogleログインでは、今回作ったデータベースを活用して、ユーザーのセッション情報を保存しながらGoogle認証を実装していきます。