⚙️

【Next.js】Prismaをつかってみたい

2025/01/12に公開

Node.js および TypeScript の ORM であるPrisma
Next.js でつかってみたいので、設定していきます!

そもそも ORM とは?

ORM(Object Relational Mapper) とは、オブジェクト指向のように RDB を操作できるもの。
SQL を書く必要がなく、プログラミング言語と RDB の間を取り持ってくれるというイメージ。

つまり、プログラム ↔ ORM ↔ RDBとなります。

あらためて Prisma とは?

https://www.prisma.io/docs/concepts/overview/what-is-prisma

こちらの公式ドキュメントによると、

プリズマは、オープンソース次世代 ORM。これは次の部分で構成されます。

  • Prisma Client : Node.js および TypeScript 用の自動生成されたタイプセーフなクエリ ビルダー

  • Prisma Migrate : 移行システム

  • Prisma Studio : データベース内のデータを表示および編集するための GUI。

Prisma Studio のみオープンソースではない点にご注意ください!

Node.js や TypeScript 用の ORMということですね。

Prisma が必要としている要件やサポートしているバージョンは下記をご覧ください!

・要件
https://www.prisma.io/docs/reference/system-requirements#system-requirements

・サポートされているフレームワーク
https://www.prisma.io/docs/concepts/overview/prisma-in-your-stack/fullstack#supported-frameworks

・サポートされている DB
https://www.prisma.io/docs/reference/database-reference/supported-databases

バージョン

バージョン
Prisma 5.2.0
Prisma Client 5.2.0
Prisma Studio 0.494.0
Node.js 18.17.0
Next.js 13.4.19
Docker 20.10.21
PostgreSQL 15.4

完成したアプリケーション

https://github.com/hisuihisui/sample-prisma-next-app

こちらのGitHub リポジトリに完成したアプリケーションを push しました。

ご参考ください!!

下準備

Next.js のアプリを作成

もととなる Next.js のサンプルアプリを作成します。

コマンド実行後にきかれるものはすべてデフォルトのままにしておきます。

$ npx create-next-app sample-prisma-next-app

作成出来たら、動くか確認しておきます

$ cd sample-prisma-next-app
$ npm run dev

http://localhost:3000 にアクセスして、表示されれば OK です!

PostgreSQL コンテナを起動

↓ を参考にコンテナを起動していきます。

https://zenn.dev/re24_1986/articles/b76c3fd8f76aec

PostgreSQL コンテナ用のフォルダを作成します。

ファイルの構成は以下とします。

PostgreSQL$ tree
.
├── config
│   └── postgresql.conf
├── docker-compose.yml
└── sql
    └── v   # コンテナの初回起動時のみ実行したいファイル

各ファイルの中身はこんな感じ

config/postgresql.conf
listen_addresses = '*'

docker-compose.yml
version: '3'

services:
  db:
    image: postgres:15.4
    container_name: postgres
    # confファイルの指定
    command: -c 'config_file=/etc/postgresql/postgresql.conf'
    ports:
      - 5432:5432
    volumes:
      # DBのデータをボリュームマウント
      - db-store:/var/lib/postgresql/data
      # sqlファイルをコンテナにバインドマウント
      # コンテナの初回起動時のみ実行されるsql
      - ./sql/init:/docker-entrypoint-initdb.d
      # 任意のSQLファイルを実行できるようにバインドマウント
      - ./sql/tmp:/tmp
      # confファイルをコンテナにバインドマウント
      - ./config/postgresql.conf:/etc/postgresql/postgresql.conf
    environment:
      # スーパーユーザーのパスワード
      - POSTGRES_PASSWORD=passw0rd
volumes:
  db-store:

init.sql
-- Prismaが使用するユーザー作成
CREATE USER prismauser WITH PASSWORD 'prismapassword';
ALTER ROLE prismauser SET CLIENT_ENCODING TO 'utf8';
ALTER ROLE prismauser SET DEFAULT_TRANSACTION_ISOLATION TO 'read committed';
ALTER ROLE prismauser SET TIMEZONE TO 'Asia/Tokyo';
-- prisma migrateする際に必要な権限
-- https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#shadow-database-user-permissions
ALTER ROLE prismauser CREATEDB;

-- DATABASE作成
CREATE DATABASE prismadb;

-- 作成したDBへ切り替え
\c prismadb

-- スキーマ作成
CREATE SCHEMA prismaschema;

-- 権限追加
GRANT CREATE ON DATABASE prismadb TO prismauser;
GRANT USAGE ON SCHEMA prismaschema TO prismauser;
GRANT CREATE ON SCHEMA prismaschema TO prismauser;
GRANT DELETE, INSERT, SELECT, UPDATE ON ALL TABLES IN SCHEMA prismaschema TO prismauser;
GRANT SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA prismaschema TO prismauser;

ユーザー名やデータベース名、スキーマ名はわかりやすいように prismaにしていますが、なんでもいいです
ファイルを作成したら、コンテナを起動して入ってみます

// コンテナを起動
PostgreSQL$ docker compose up -d
[+] Running 3/3
 ✔ Network postgresql_default    Created                                                                                                                                                                          0.0s
 ✔ Volume "postgresql_db-store"  Created                                                                                                                                                                          0.0s
 ✔ Container postgres            Started

// コンテナに入る
PostgreSQL$ docker exec -it postgres /bin/sh

// PostgreSQLにログイン
# psql -h localhost -U prismauser prismadb
psql (15.4 (Debian 15.4-1.pgdg120+1))
Type "help" for help.

// スキーマを確認
prismadb=> \dn;
         List of schemas
     Name     |       Owner
--------------+-------------------
 prismaschema | postgres
 public       | pg_database_owner
(2 rows)

// スキーマを確認
prismadb=> select prismaschema;
// テーブル確認:作ってないのでない
prismadb-> \dt;
Did not find any relations.
// ログアウト
prismadb-> \q

// コンテナから抜ける
# exit
PostgreSQL$

うまく起動できているのでこれで OK です!

Prisma をつかってみる

Next.js アプリのルートディレクトリに移動して下さい

Prisma をインストール

// インストール
sample-prisma-next-app$ npm install prisma --save-dev

added 2 packages, and audited 332 packages in 6s

117 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

// 確認
sample-prisma-next-app$ npx prisma --version
prisma                  : 5.2.0
@prisma/client          : Not found
Current platform        : debian-openssl-3.0.x
Query Engine (Node-API) : libquery-engine 2804dc98259d2ea960602aca6b8e7fdc03c1758f (at node_modules/@prisma/engines/libquery_engine-debian-openssl-3.0.x.so.node)
Schema Engine           : schema-engine-cli 2804dc98259d2ea960602aca6b8e7fdc03c1758f (at node_modules/@prisma/engines/schema-engine-debian-openssl-3.0.x)
Schema Wasm             : @prisma/prisma-schema-wasm 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f
Default Engines Hash    : 2804dc98259d2ea960602aca6b8e7fdc03c1758f
Studio                  : 0.494.0

--save-dev をつけるのはなぜなのかは ↓ の記事を読んでみるとわかると思います

https://qiita.com/kohecchi/items/092fcbc490a249a2d05c

https://qiita.com/chihiro/items/ca1529f9b3d016af53ec

https://mo-gu-mo-gu.com/about-dependencies-devdependencies/

Prisma をセットアップ

npx prisma init

↑ のコマンドを実行します。

PostgreSQL 以外を使用する方は、datasource-provider オプションで指定します。(参考

これによって

  1. prisma フォルダとその配下に schema.prisma を作成
  2. .env を作成 ※git にコミットしないように.gitignore に.env を追記しましょう

がなされます。

(実行結果)

sample-prisma-next-app$ npx prisma init

<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="✔" src="https://s.w.org//images/core/emoji/14.0.0/svg/2714.svg">" src="https://s.w.org//images/core/emoji/14.0.0/svg/2714.svg"> Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

// フォルダやファイルが作成されたか確認
sample-prisma-next-app$ ll prisma/
total 0
drwxrwxrwx 1 ****** ****** 4096 Aug 28 23:44 ./
drwxrwxrwx 1 ****** ****** 4096 Aug 28 23:44 ../
-rwxrwxrwx 1 ****** ******  236 Aug 28 23:44 schema.prisma*
sample-prisma-next-app$ ll .env
-rwxrwxrwx 1 ****** ****** 519 Aug 28 23:44 .env*

DB の接続情報を設定する

先ほど作成された schema.json の中身を見てみます

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

このうち、DB に関する記述は 8~11 行目の datasource db の部分になります
url は値を直書きせずに、DATABASE_URL という環境変数から読み取るようになっています

そこで、schema.json と一緒に作成された .env ファイルをみます

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

DATABASE_URL が定義されているので、値を書き換えていきます。

PostgreSQL の場合は、下記のフォーマットになっています

// 大文字:人によって値が異なるため、適宜書き換える
postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA

.env を

DATABASE_URL="postgresql://prismauser:prismapassword@localhost:5432/prismadb?schema=prismaschema"

に書き換えます

Prisma Migrate してみる

schema.prisma で Data Model を定義して、DB に反映してみます

まず、schema.prisma に Data Model を定義します

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

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   @db.VarChar(255)
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  user   User    @relation(fields: [userId], references: [id])
  userId Int     @unique
}

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

↓ のコマンドで schema.prisma の内容を DB に反映させます。

npx prisma migrate dev --name init

これで2つのことが実行されます

  1. schema.prisma をもとに、DB に実行する用の SQL ファイルを作成
  2. 1. で作成された SQL ファイルを DB に実行する

(実行結果)

sample-prisma-next-app$ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "prismadb", schema "prismaschema" at "localhost:5432"

Applying migration `20230903073559_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20230903073559_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

added 2 packages, and audited 334 packages in 7s

117 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

✔ Generated Prisma Client (v5.2.0) to ./node_modules/@prisma/client in 343ms


エラーが出た方は ↓ を参考にしてみてください

https://zenn.dev/kuuki/articles/prisma-migrate-error-resolve/

成功したら、prisma フォルダに migrations フォルダと SQL ファイルが作成されています

sample-prisma-next-app$ tree prisma
prisma
├── migrations
│   ├── 20230903073559_init
│   │   └── migration.sql  # これが作成・実行されたSQLファイル
│   └── migration_lock.toml
└── schema.prisma

migration.sql を見てみると、

schema.prisma で定義した3 つの Data Model に対応する 3 つのテーブルを作成しています

-- CreateTable
CREATE TABLE "Post" (
    "id" SERIAL NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,
    "title" VARCHAR(255) NOT NULL,
    "content" TEXT,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "authorId" INTEGER NOT NULL,

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

-- CreateTable
CREATE TABLE "Profile" (
    "id" SERIAL NOT NULL,
    "bio" TEXT,
    "userId" INTEGER NOT NULL,

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

-- CreateTable
CREATE TABLE "User" (
    "id" SERIAL NOT NULL,
    "email" TEXT NOT NULL,
    "name" TEXT,

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

-- CreateIndex
CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId");

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

Prisma Client をインストール

Prisma を Next.js のアプリから呼び出せるように Prisma Client をインストールします

npm install @prisma/client

こちらのコマンドを実行すると、

  1. パッケージのインストール
  2. prisma generate : schema.prisma で定義した内容を Prisma Client へインポート

を行います。

prisma migrate や prisma db push を実行した際にも prisma generate が実行され、Prisma Client が更新されます

(実行結果)

sample-prisma-next-app$ npm install @prisma/client

up to date, audited 334 packages in 3s

117 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Prisma Client をつかってみる

Next.js アプリ内でPrisma Client を使って、DB へのデータの読み書きを行います

下記のようなアプリケーションを作成してみます

  • プロフィールとともにユーザーを登録する
  • 登録されているユーザー一覧を取得し、表示する

ディレクトリ構成

src
├── app
│   ├── api
│   │   └── user
│   │       └── route.ts   # ファイル作成
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── addUser.tsx        # ファイル作成
│   └── userList.tsx       # ファイル作成
└── lib
    └── prisma.ts          # ファイル作成

コード

・src/app/global.css

これがあると画面が見づらいので中身を空にしておきます

src/app/page.tsx

後述するコンポーネントを呼び出します

import AddUser from "@/components/addUser";
import UserList from "@/components/userList"

export default async function Home() {
  return (
    <div>
      {/* User登録を行うコンポーネント */}
      <AddUser />
      {/* User一覧を表示するコンポーネント */}
      <UserList />
    </div>
  );
}

src/app/user/route.ts

Prisma Client を使用して DB とやり取りする処理を API 化しておく

http://localhost:3000/api/user がエンドポイントになります

import { NextRequest, NextResponse } from "next/server";

// Prisma Clientのインスタンスをインポート
import prisma from "@/lib/prisma";

export async function GET() {
  const userArray = await prisma.user.findMany({
    include: {
      // true: 外部キーを設定しているテーブルの情報も一緒に持ってくる
      posts: true,
      profile: true,
    },
  });

  // Response を jsonで返す
  return NextResponse.json(userArray);
}

export async function POST(req: NextRequest) {
  // リクエストボディ
  const { name, email, postTitle, profile } = await req.json();

  const res = await prisma.user.create({
    data: {
      name: name,
      email: email,
      // ネストすることで参照先のテーブルに書き込める
      // Postテーブルに書き込む
      posts: {
        create: { title: postTitle },
      },
      // Profileテーブルに書き込む
      profile: {
        create: { bio: profile },
      },
    },
  });

  return NextResponse.json(res);
}

src/components/addUser.tsx

ユーザー登録を行うコンポーネント

// input タグのonChangeを使うためにServer Componentにする
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";

export default function AddUser() {
  const router = useRouter();

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [postTitle, setPostTitle] = useState("");
  const [profile, setProfile] = useState("");

  // Userテーブルへデータを書き込む
  // 注意:emailが重複禁止となっている
  const fetchAsyncAddUser = async () => {
    // 入力されていないものがあれば、登録しない
    if (name == "" || email == "" || postTitle == "" || profile == "") {
      console.log("すべての項目を埋めてください");
      return;
    }

    // APIのURL
    const url = "http://localhost:3000/api/user";
    // リクエストパラメータ
    const params = {
      method: "POST",
      // JSON形式のデータのヘッダー
      headers: {
        "Content-Type": "application/json",
      },
      // リクエストボディ
      body: JSON.stringify({
        name: name,
        email: email,
        postTitle: postTitle,
        profile: profile,
      }),
    };

    // APIへのリクエスト
    await fetch(url, params);

    // 入力値を初期化
    setName("");
    setEmail("");
    setPostTitle("");
    setProfile("");

    // 画面をリフレッシュ
    router.refresh();
  };

  // inputタグのvalueに変化があった際に実行される
  const changeNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };

  // inputタグのvalueに変化があった際に実行される
  const changeEmailInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  // inputタグのvalueに変化があった際に実行される
  const changePostTitleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPostTitle(e.target.value);
  };

  // inputタグのvalueに変化があった際に実行される
  const changeProfileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setProfile(e.target.value);
  };

  return (
    <div>
      <h2>Add User</h2>
      <div>
        <div>
          <label>Name:</label>
          <input
            type="text"
            name="name"
            value={name}
            onChange={changeNameInput}
          />
        </div>
        <div>
          <label>Email:</label>
          <input
            type="text"
            name="email"
            value={email}
            onChange={changeEmailInput}
          />
        </div>
        <div>
          <label>Post Title:</label>
          <input
            type="text"
            name="postTitle"
            value={postTitle}
            onChange={changePostTitleInput}
          />
        </div>
        <div>
          <label>Profile:</label>
          <input
            type="text"
            name="profile"
            value={profile}
            onChange={changeProfileInput}
          />
        </div>
        <div>
          <button onClick={fetchAsyncAddUser}>追加</button>
        </div>
      </div>
    </div>
  );
}

src/components/userList.tsx

ユーザー一覧を表示するコンポーネント

export default async function UserList() {
  // APIのURL
  const url = "http://localhost:3000/api/user";
  // APIへリクエスト
  const res = await fetch(url, {
    cache: "no-store",
  });
  // レスポンスボディを取り出す
  const data = await res.json();

  return (
    <div>
      <h2>All Users</h2>
      {data.map((user: any, index: any) => (
        <div key={index}>
          Name: {user.name} Email: {user.email} Posts:
          {user.posts.map((value: any) => `${value.title},`)} Profile:
          {user.profile?.bio}
        </div>
      ))}
    </div>
  );
}

src/lib/prisma.ts

ベストプラクティスに沿って、Prisma Client をシングルトンにしておきます

このファイルでのみ Prisma Client をインスタンス化し、ほかのファイルから呼び出します

https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices

// Prisma Clientのインポート
import { PrismaClient } from "@prisma/client";

const prismaClientSingleton = () => {
  return new PrismaClient({
    log: ["query"],
  });
};

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined;
};

const prisma = globalForPrisma.prisma ?? prismaClientSingleton();

export default prisma;

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

動作確認

できたら、サーバーを立ち上げます

npm run dev

http://localhost:3000/ にアクセスして、↓ のように表示されると思います

各項目を埋めて「追加」ボタンをクリックしてください。

All Users の下に登録したユーザーが表示されれば、データの読み書きともにうまくいってそうです。

(例えば ↓ のような感じです)

動作確認が終わったら、Ctrl + C 等でアプリを終了させてください

Prisma Studio をつかってみる

**Prisma Studio(データベース内のデータを表示および編集するための GUI)**を起動させてみます。

npx prisma studio

すると、ブラウザに自動で表示されます

ここから、テーブルやレコードの状態を確認できます

最後に

Prisma のチュートリアルを通して、Next.js で使ってみました

OR Mは便利ですねーー!

Prisma を GraphQL と組み合わせても面白そうなので、挑戦してみます。

本記事でさくせいしたアプリを Vercel へデプロイしてみたい方はこちらもチェック

https://zenn.dev/kuuki/articles/nextjs-use-prisma-postgresql-deploy-to-vercel/

参考文献

https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch

https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#shadow-database-user-permissions

https://zenn.dev/re24_1986/articles/b76c3fd8f76aec

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

Discussion