Next.js プロジェクトに Prisma を導入する方法と基本操作まとめ


## はじめに

本記事では、Prisma をプロジェクトに追加する方法と、スキーマ定義や使い方について解説します。
[Prisma](https://www.prisma.io/) は、TypeScript 向けの ORM（Object-Relational Mapping）です。

#### 開発環境

- Node.js v20.11.1 以上
- npm v9.x 以上

---

## Prisma の初期設定

Prisma をプロジェクトに追加する際の初期設定を行います。
カレントディレクトリをプロジェクトフォルダに変更（例：`cd my-project`）し、以下のコマンド操作を進めます。

#### インストール

Prisma の依存関係をインストールします。

```bash
npm install prisma@^6 @prisma/client@^6
```

- prisma@^6：Prisma CLI。マイグレーションやモデルの生成などを行う開発用ツール。
- @prisma/client@^6：アプリケーションから DB アクセスするための Prisma クライアント。

#### 初期化

以下のコマンドを実行すると、`prisma/schema.prisma` と `.env` が生成されます。

```bash
npx prisma init
```

DB に sqlite を使用する場合、以下の通り設定します。

```: prisma/schema.prisma
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
```

```: .env
DATABASE_URL="file:./dev.db"
```

---

## Prisma モデルの設定

`prisma/schema.prisma` に Prisma モデルを記述します。
それぞれの定義内容を順番に見ていきます。

#### Prisma モデル: User

###### 1. スキーマ定義

```: prisma/schema.prisma
model User {
  id        String    @id @default(cuid())
  email     String    @unique
  password  String
  userName  String
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}
```

###### 2. 定義詳細

| フィールド名 | 日本語名       | 型         | 属性                   | 制約             |
| ------------ | -------------- | ---------- | ---------------------- | ---------------- |
| `id`         | ユーザー ID    | `String`   | `@id @default(cuid())` | 主キー、自動生成 |
| `email`      | メールアドレス | `String`   | `@unique`              | 一意制約         |
| `password`   | パスワード     | `String`   | なし                   | 必須             |
| `userName`   | ユーザー名     | `String`   | なし                   | 必須             |
| `posts`      | 投稿一覧       | `Post[]`   | リレーション           | 1 対多関係       |
| `createdAt`  | 作成日時       | `DateTime` | `@default(now())`      | 自動設定         |
| `updatedAt`  | 更新日時       | `DateTime` | `@updatedAt`           | 自動更新         |

補足説明：

- `@id`: 主キー（Primary Key）を示す。
- `@default(...)`: デフォルト値。 `cuid()` は一意な文字列を生成する。
- `@unique`: 一意制約。重複を許さない。
- `@updatedAt`: レコードが更新されるたびに、このフィールドが自動で更新される。
- `[]`: 他のモデルとの 1 対多のリレーション（外部キー制約）を示す。

#### Prisma モデル: Post

###### 1. スキーマ定義

```: prisma/schema.prisma
model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  topImage  String?
  published Boolean  @default(true)
  authorId  String
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
```

###### 2. 定義詳細

| フィールド名 | 日本語名         | 型         | 属性                                                                 | 制約                   |
| ------------ | ---------------- | ---------- | -------------------------------------------------------------------- | ---------------------- |
| `id`         | 投稿 ID          | `String`   | `@id @default(cuid())`                                               | 主キー、自動生成       |
| `title`      | タイトル         | `String`   | なし                                                                 | 必須                   |
| `content`    | 本文             | `String`   | なし                                                                 | 必須                   |
| `topImage`   | アイキャッチ画像 | `String?`  | なし                                                                 | 任意（null 許容）      |
| `published`  | 公開フラグ       | `Boolean`  | `@default(true)`                                                     | デフォルト: `true`     |
| `authorId`   | 著者 ID          | `String`   | なし                                                                 | 必須                   |
| `author`     | 著者             | `User`     | `@relation(fields: [authorId], references: [id], onDelete: Cascade)` | リレーション、削除連鎖 |
| `createdAt`  | 作成日時         | `DateTime` | `@default(now())`                                                    | 自動設定               |
| `updatedAt`  | 更新日時         | `DateTime` | `@updatedAt`                                                         | 自動更新               |

###### 3. 外部キー制約の説明

`author` の属性は以下のように定義しています。

```prisma
@relation(fields: [authorId], references: [id], onDelete: Cascade)
```

このアノテーションは、

モデル `Post` にある `authorId` は、
モデル `User` の `id` を参照しており、
もしその `User` が削除されたら、紐づく `Post` も 自動的に削除される（**Cascade**）

というリレーションです。

| オプション           | 説明                                                                                                                                        |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `fields: [authorId]` | 現在のモデル（この場合 `Post`）で外部キーとして使うフィールドを指定します。このフィールドが、他のモデルの主キー等と一致する値を保持します。 |
| `references: [id]`   | リレーション先のモデル（この場合 `User`）の、対応するフィールド（通常は主キー）を指定します。                                               |
| `onDelete: Cascade`  | リレーション先（この場合 `User`）のデータが削除された時の動作を定義します。この場合は「連動して `Post` も削除される」という設定です。       |

この定義により、以下のことが保証されます：

- `Post` の `authorId` は、必ず存在する `User.id` に一致する必要がある（外部キー制約）。
- ある `User` を削除すると、そのユーザーが書いたすべての `Post` も自動的に削除される（Cascade）。

---

## Prisma + Next.js でブログ記事一覧を取得する（Demo）

Next.js プロジェクトで Prisma を使って、以下の内容を実践します

- Prisma クライアントの設定
- スキーマの作成とマイグレーション
- ダミーデータ（seed）の挿入
- Prisma Studio を使ったデータ確認
- データベースからデータを取得して画面に表示

プロジェクトの構成は以下の通りです。

```bash
my-project/
├── prisma/
│   ├── data/
│   │   └── dummyData.js
│   ├── schema.prisma
│   └── seed.cjs
└── src/
    ├── app/
    │   ├── globals.css
    │   └── blog/
    │       └── page.tsx
    └── lib/
        └── prisma.ts
```

#### 1. Prisma Client の設定

まずは Prisma のクライアントを設定します。

```ts: prisma.ts
// PrismaClient の型定義をインポート
import { PrismaClient } from "@/generated/prisma";

// グローバルオブジェクトに prisma プロパティを追加
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

// 既存インスタンスがあれば再利用し、なければ新規作成
export const prisma = globalForPrisma.prisma || new PrismaClient();

// 開発環境ではインスタンスをグローバルに保持
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
```

Next.js の開発環境では、ホットリロード時に PrismaClient が複数生成されるのを防ぐため、グローバルにインスタンスを保持するのが一般的です。

#### 2. Prisma スキーマの設定

Prisma クライアントを src/generated/prisma に出力するよう指定します。

```: schema.prisma
generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}
```

#### 3. シードスクリプト（ダミーデータ挿入）

ダミーデータを挿入する seed.cjs スクリプトです。

```js: seed.cjs
// Prisma Client を読み込む（Prisma を使って DB にアクセスするためのライブラリ）
const { PrismaClient } = require("../src/generated/prisma");

// パスワードをハッシュ化するためのライブラリ（bcryptjs）
const bcrypt = require("bcryptjs");

// ダミーデータ
const { DUMMY_PASSWORD, USER_DATA, POST_DATA } = require("./data/dummyData");

const prisma = new PrismaClient(); // Prisma Client インスタンス

async function seed() {
  // 既存データを削除（クリーンアップ）
  await prisma.post.deleteMany(); // 投稿データを全削除
  await prisma.user.deleteMany(); // ユーザーデータを全削除

  // パスワードをハッシュ化
  const password = await bcrypt.hash(DUMMY_PASSWORD, 12);

  // ユーザー＋投稿を一括作成
  const user = await prisma.user.create({
    data: {
      ...USER_DATA,
      password,
      posts: {
        create: POST_DATA,
      },
    },
  });

  console.log("Seeded user:", user);
}

seed()
  .catch((error) => {
    console.error("Seeding failed:", error);
    process.exit(1); // 終了コード1でプロセス終了
  })
  .finally(async () => {
    await prisma.$disconnect(); // Prisma クライアントを閉じる（リソース解放）
  });
```

:::note info
**シードスクリプトを `.cjs` で作成する理由**

Node.js では、ファイルの拡張子によってモジュールの扱いが変わります。
拡張子を `.js` にすると、デフォルトでは ES モジュール（ESM） として解釈されることがあります（特に type: "module" が package.json にある場合）。
拡張子を `.cjs` にすると、CommonJS モジュールとして明示的に扱われます。

Prisma の seed スクリプトは、初期状態では CommonJS 形式で動作する前提のため、拡張子を `.cjs` にしてトラブル回避しています。
:::

#### 4. package.json に seed スクリプトを追加

```json: package.json
  "prisma": {
    "seed": "node prisma/seed.cjs"
  }
```

#### 5. DB セットアップとダミーデータ挿入を実行

以下のコマンドを順番に実行することで、データベースのセットアップとダミーデータの挿入が完了します。

```bash
# Prisma スキーマをもとにマイグレーション（テーブル作成）
npx prisma migrate dev --name init

# Prisma クライアントの生成
npx prisma generate

# ダミーデータの挿入（seed）
npx prisma db seed

# Prisma Studio で DB 内容をGUI確認
npx prisma studio
```

#### 6. データベースからデータを取得して画面に表示

データベースに保存された Post の内容を取得し、画面に表示します。

```tsx: @/app/blog/page.tsx
import { prisma } from "@/lib/prisma";

const BlogContents = async () => {
  // データベースから全ての投稿を取得
  const contents = await prisma.post.findMany({
    orderBy: { createdAt: "desc" },
    include: { author: true }, // リレーション先の投稿者情報も取得
  });

  return (
    <>
      <div className="overflow-x-auto">
        <p>ブログ記事一覧</p>
        <table className="min-w-full divide-y divide-gray-200">
          <thead className="bg-gray-100">
            <tr>
              <th className="th-style">タイトル</th>
              <th className="th-style">内容</th>
              <th className="th-style">著者</th>
            </tr>
          </thead>
          <tbody className="bg-white divide-y divide-gray-200">
            {contents.map((content, index) => (
              <tr key={index} className="hover:bg-gray-50">
                <td className="td-style">{content.title}</td>
                <td className="td-style">{content.content}</td>
                <td className="td-style">
                  {content.author.userName}/{content.author.email}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
};

export default BlogContents;
```

http://localhost:3000/blog にアクセスすると、作成したダミーデータが画面に表示されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665246/7f833ce0-835d-4e62-b783-1c8b69962b94.png)

#### （補足）スタイリングとダミーデータの中身

この記事の本題は Prisma の使い方ですが、画面表示やシード作成に使った CSS クラスやダミーデータの中身についても、参考として簡単に紹介しておきます。

###### Tailwind CSS のカスタムクラス定義

Tailwind CSS のユーティリティクラスを同じ要素に繰り返し適用している場合、共通化の方法がいくつか用意されています。
今回は `globals.css` に `@apply` ディレクティブを使ってカスタムクラスを定義し、`<th>` タグと `<td>` タグに設定しています。

```css: globals.css
@import "tailwindcss";

.th-style {
  @apply px-6 py-3 text-left text-xs font-medium text-gray-500;
}

.td-style {
  @apply px-6 py-4 whitespace-nowrap text-sm text-gray-900;
}
```

###### ダミーデータの中身

シードスクリプトで使っているダミーデータです。

```js: prisma/data/dummyData.js
// ユーザーパスワード
const DUMMY_PASSWORD = "p@ssw0rd1234";

// メールアドレスとユーザー名
const USER_DATA = {
  email: "test@example.com",
  userName: "nanashi",
};

// 投稿データ
const POST_DATA = [
  {
    title: "Prismaとは",
    content: "TypeScript 向けの ORM（Object-Relational Mapping）",
  },
  {
    title: "Next.js の Server Actions について",
    content: "Server Actions を使うメリットを解説します！",
  },
];

module.exports = {
  DUMMY_PASSWORD,
  USER_DATA,
  POST_DATA,
};
```

---

## まとめ

本記事では、Prisma のプロジェクトへの追加方法、スキーマ定義や使い方について簡単に説明しました。
Prisma を使うことで、スキーマ定義 → テーブル作成 → クライアント生成 → ダミーデータ投入までが簡潔に行えますので、開発効率を向上させることができます。

最後に、Prisma モデルの定義でよく使うパラメータをまとめておきます。

#### よく使う Prisma の属性

| 属性名（アノテーション） | 用途・意味                                                                       |
| ------------------------ | -------------------------------------------------------------------------------- |
| `@id`                    | 主キー（Primary Key）を指定する。1 つのモデルに 1 つのみ設定可能。               |
| `@default(...)`          | フィールドのデフォルト値を設定。`now()`、`uuid()`、`cuid()`などが使用可能。      |
| `@unique`                | 一意制約（UNIQUE）を設定。同じ値の重複登録を禁止する。                           |
| `@updatedAt`             | レコード更新時にこの日時フィールドを自動更新。`DateTime`型と併用。               |
| `@relation(...)`         | 他のモデルとのリレーションを定義。外部キーの指定やリレーションの挙動を制御する。 |

###### よく使う `@default` の引数

| 引数名     | 説明                                      |
| ---------- | ----------------------------------------- |
| `now()`    | 現在時刻（`DateTime`型）を設定            |
| `uuid()`   | UUID（RFC4122）形式の文字列を生成         |
| `cuid()`   | 一意な文字列 ID（短く可読性が高い）を生成 |
| リテラル値 | 例: `true`, `"text"`, `0`, `[]`など       |

###### `@relation` の詳細オプション

| オプション名        | 説明                                                                       |
| ------------------- | -------------------------------------------------------------------------- |
| `fields: [...]`     | 外部キーとなるローカルのフィールド名を指定。                               |
| `references: [...]` | 外部のテーブルのどのカラムを参照するかを指定。                             |
| `onDelete`          | 外部キーの対象が削除されたときの動作。例: `Cascade`, `SetNull`, `Restrict` |
| `onUpdate`          | 外部キーの対象が更新されたときの動作。通常は `Restrict` や `Cascade`       |
| `name: "..."`       | リレーション名を明示的に設定。省略可能。                                   |