Prismaを導入しようとしたら、公式の罠にはめられた

# はじめに

　Webアプリ初心者で、DB操作のためにPrismaを使ってみようと思って導入していたところ、罠にはめられて夜神ライトになったというお話を、Prismaの導入方法と合わせて書いていきます。

# 目次

- [はじめに](#はじめに)
- [目次](#目次)
- [環境](#環境)
  - [各バージョン](#各バージョン)
  - [導入前の状態](#導入前の状態)
    - [ディレクトリ構成](#ディレクトリ構成)
    - [各ファイルの内容](#各ファイルの内容)
- [本編: Prisma導入](#本編-prisma導入)
  - [1. インストールと初期設定](#1-インストールと初期設定)
    - [1.1. インストール](#11-インストール)
    - [1.2. 初期設定](#12-初期設定)
  - [2. Prisma Migrationでテーブル作成](#2-prisma-migrationでテーブル作成)
    - [2.1. スキーマ定義](#21-スキーマ定義)
    - [2.2. テーブル作成](#22-テーブル作成)
      - [罠1: 権限不足](#罠1-権限不足)
  - [3. JavaScriptコードから、DBクエリを実行](#3-javascriptコードからdbクエリを実行)
    - [3.1. 準備](#31-準備)
    - [3.2. クエリ実行](#32-クエリ実行)
      - [罠2: 出力ディレクトリと importパスの相違](#罠2-出力ディレクトリと-importパスの相違)
- [さいごに](#さいごに)

# 環境

## 各バージョン

- Node.js v23.11.0
- MySQL2 v3.14.0
- Prisma v6.5.0
- Docker v28.0.4
  - サーバー用、DB用コンテナを用意

## 導入前の状態

<details><summary>必要最低限のものだけ載せています。</summary>

- JSを使用していますが、TSでも変わりません
- Dockerを使用していますが、他の状況でも変わらないと思います

### ディレクトリ構成

```
.
├── db/
│   ├── .env
│   └── conf.d/
│       └── my.cnf
├── server/
│   ├── node_modules/
│   ├── src/
│   │   └── server.mjs
│   ├── Dockerfile
│   ├── package.json
│   └── package-lock.json
└── docker-compose.yaml
```

### 各ファイルの内容

```db/.env
MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=password
MYSQL_USER=dbuser
MYSQL_PASSWORD=password
MYSQL_DATABASE=hoge_database
```

```json:server/package.json
{
  "name": "projeck-backend",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "node src/server.mjs",
    "dev": "nodemon --watch 'src/**/*.mjs' --exec 'node src/server.mjs'"
  },
  "dependencies": {
    "express": "^4.21.2",
    "mysql2": "^3.14.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.9",
  }
}
```

```dockerfile:server/dockerfile
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install -g npm
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
```

```yaml:./docker-compose.yaml
services:

  server:
    build: ./server
    ports:
      - "3000:3000"
    volumes:
      - ./server:/app
      - /app/node_modules
    environment:
      - CHOKIDAR_USEPOLLING=true
    tty: true
    networks:
      - backend
    depends_on:
      - db
  
  db:
    image: mysql:8.0
    env_file:
      - db/.env
    ports:
      - "3306:3306"
    volumes:
      - ./db/conf.d:/etc/mysql/conf.d
      - db-data:/var/lib/mysql
    networks:
      - backend

networks:
  backend:

volumes:
  db-data:
```

</details>

# 本編: Prisma導入

　基本は以下の公式ドキュメント通りに進めていきましたが、この通りにやるだけだと進めなかったので、解決方法を入れつつ書いていきます。

https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-node-mysql

## 1. インストールと初期設定

### 1.1. インストール

　`server`ディレクトリ内で作業していきます。まずはインストールから。

```bash
$ cd server
$ npm install prisma --save-dev
```

　以下のコマンドでエラーが出なければ、インストール成功です。

```bash
$ npx prisma
```

### 1.2. 初期設定

　次のコマンドで、Prismaスキーマファイルを作成します。

```bash
$ npx prisma init --datasource-provider mysql --output ../generated/prisma
```

　実行すると、以下のファイルが生成されます。

```
server/
├── ...
├── .env
└── prisma/
    └── schema.prisma
```

```.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="mysql://johndoe:randompassword@localhost:3306/mydb"
```

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

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

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

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

　`schema.prisma`は、Prismaの設定やSQLテーブルのスキーマ設定を行うファイルです。重要なのは、
- `datasource db`
  - `provider`：DBの種類を指定する
    - デフォルトは`postgresql`
    - `--datasource-provider`オプションで`mysql`に設定された
  - `url`：DBへ接続するための情報を指定する
- `generator client`
  - `output`：設定したテーブル用のAPIを生成する場所を指定する
    - デフォルトは設定なし（node_modules内）
    - `--output`オプションで設定された

ということです。

　DBへ接続できるようにするため、`.env`の`DATABASE_URL`を設定します。`db/.env`の内容に合わせて、（環境変数を使いまわす方法がわからなかったので）べた書きしました。
```.env
# "DBの種類://ユーザ名:パスワード@接続先ホスト:ポート番号/DB名"
DATABASE_URL="mysql://dbuser:password@db:3306/hoge_database"
```

<details><summary>接続先ホストについて</summary>
　今回は、docker-composeを用いて環境構築をしているため、サービス名の`db`を指定してます。DBがどこにあるのかに合わせて指定する必要があります。

- ローカル環境：`localhost`
- リモート環境：IPアドレスなど
- Docker Networkで接続されたコンテナ：コンテナ名

</details>

---
　初期設定はこれで完了しました。公式ドキュメントに忠実に従いましたが、既に罠が仕込まれています。発動するのはまだ少し先です。

## 2. Prisma Migrationでテーブル作成

　次は、DBに接続してテーブルを作成します。

### 2.1. スキーマ定義

　Prismaでは、`schema.prisma`にテーブルモデルを記述し、それを実際のDBスキーマに反映します（Prisma Migration）。ドキュメントの例にあるモデルを追加しました。

<details><summary>テーブルモデル追加</summary>

```ts:prisma/schema.prisma
// ...（省略）

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?
}
```

</details>

### 2.2. テーブル作成

　テーブルモデルができたので、DBにテーブルを追加していきます。（私はDockerを使用していますが、関係ない箇所の操作は省略します。）

　次のコマンドを実行する[^container]ことで、接続先DBにテーブルを作成することが<font color="red">できません</font>。
[^container]: Dockerの場合は、コンテナ内で実行する。

```bash
$ npx prisma migrate dev --name init
```

:::note info

`dev`：開発環境用のコマンド
`--name`：モデル変更の説明用コメントを付けられる

:::

　実行すると、以下のようなエラーが出ました。

<details><summary>実行結果</summary>

```bash
$ npx prisma migrate dev --name init

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "hoge_database" at "db:3306"

Error: P3014

Prisma Migrate could not create the shadow database. Please make sure the database user has 
permission to create databases. Read more about the shadow database (and workarounds) at 
https://pris.ly/d/migrate-shadow

Original error: Error code: P1010

User `dbuser` was denied access on the database `hoge_database`
```

</details>

#### 罠1: 権限不足

> 　Prisma Migrateはシャドウデータベースを作成できませんでした。データベース ユーザーにデータベースを作成する権限があることを確認してください。シャドウデータベース（および回避策）については、https://pris.ly/d/migrate-shadow を参照してください。
　ユーザー `dbuser` はデータベース `hoge_database` へのアクセスを拒否されました。

　記載されているURLにアクセスしてみました。

　`prisma migration dev`コマンドを実行すると、
1. シャドウデータベースを使用し、スキーマ変更履歴から現在の状態を生成する
1. 生成したものと実態を比較し、一致しない場合はスキーマドリフトを報告する
1. スキーマドリフトが検出されなかった場合、今回の変更をDBに適用する

という流れをとるようです。このシャドウデータベースを操作するための権限が`dbuser`に無いため、失敗してしまいました。

　同じページに、
> MySQL/MariaDB	データベースユーザーには CREATE, ALTER, DROP, REFERENCES ON \*.\* 権限が必要です

と書いてあるので、MySQLの`root`ユーザから権限を与えます。[^container]

```bash
$ mysql -u root -p
mysql> GRANT CREATE, ALTER, DROP, REFERENCES ON *.* TO `dbuser`@`%`;
```

　権限を与えたので、もう一度コマンドを実行してみます。
<details><summary>実行結果</summary>

```bash
$ npx prisma migrate dev --name init

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "hoge_database" at "db:3306"

Applying migration `20250414164608_init`

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

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

Your database is now in sync with your schema.

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

✔ Generated Prisma Client (v6.6.0) to ./generated/prisma in 74ms
```

</details>

今度は成功しました。

　DBを直接確認すると、しっかりテーブルが作成されていることが確認できました。

<details><summary>テーブル情報確認</summary>

```bash
mysql> SHOW TABLES;
+-------------------------+
| Tables_in_hoge_database |
+-------------------------+
| Post                    |
| Profile                 |
| User                    |
| _prisma_migrations      |
+-------------------------+
4 rows in set (0.01 sec)

mysql> DESCRIBE Post;
+-----------+--------------+------+-----+----------------------+-------------------+
| Field     | Type         | Null | Key | Default              | Extra             |
+-----------+--------------+------+-----+----------------------+-------------------+
| id        | int          | NO   | PRI | NULL                 | auto_increment    |
| createdAt | datetime(3)  | NO   |     | CURRENT_TIMESTAMP(3) | DEFAULT_GENERATED |
| updatedAt | datetime(3)  | NO   |     | NULL                 |                   |
| title     | varchar(255) | NO   |     | NULL                 |                   |
| content   | varchar(191) | YES  |     | NULL                 |                   |
| published | tinyint(1)   | NO   |     | 0                    |                   |
| authorId  | int          | NO   | MUL | NULL                 |                   |
+-----------+--------------+------+-----+----------------------+-------------------+
7 rows in set (0.00 sec)

mysql> DESCRIBE Profile;
+--------+--------------+------+-----+---------+----------------+
| Field  | Type         | Null | Key | Default | Extra          |
+--------+--------------+------+-----+---------+----------------+
| id     | int          | NO   | PRI | NULL    | auto_increment |
| bio    | varchar(191) | YES  |     | NULL    |                |
| userId | int          | NO   | UNI | NULL    |                |
+--------+--------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

mysql> DESCRIBE User;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int          | NO   | PRI | NULL    | auto_increment |
| email | varchar(191) | NO   | UNI | NULL    |                |
| name  | varchar(191) | YES  |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
```
---
</details>

　成功すると、以下のファイルが生成されました。
```bash
server/
├── ...
├── .env
├── prisma/
│   ├── schema.prisma
│   └── migrations/ --------------- # New
│       ├── 20250414164608_init/ -- # New
│       │   └── migration.sql ----- # New
│       └── migration_lock.toml --- # New
└── generated/ -------------------- # New
    └── prisma/ ------------------- # New
        └── ...
```

:::note info
`migrations/`の中では、スキーマの変更履歴を確認することができます。ディレクトリ名（`20250414164608_init`）で変更日時[^time]とコメントを、`migration.sql`で実行されたSQL文を確認できます。
:::

（`generated/prisma/`については、次の章で説明します。）

[^time]: 実行環境のタイムゾーンが適用される。

---

　テーブルの作成については、これで可能になりました。スキーマを変更する際には、この章の手順を繰り返すことになります。

## 3. JavaScriptコードから、DBクエリを実行

　次は、Prismaを使用してJSからDBクエリを実行できるようにします。

### 3.1. 準備

　JSで使用できるようにするためには、Prisma Clientをインストールし、`prisma generate`コマンドを実行する必要があります。しかし、実は前の章の`prisma migrate`コマンドを実行した際に、これらの工程も一緒に実行されています。`genetated/prisma/`は、`prisma generate`コマンドによって生成されたものです。

<details><summary>ディレクトリが生成されていない場合</summary>

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

```bash
$ npm install @prisma/client
$ npx prisma generate
```
<br>
</details>

:::note info
`generated/prisma/`[^output]には、JS/TS用のORMクライアント（Prisma Client）が生成されます。DB接続・クエリにかかわるものや、DBスキーマに基づくモデルクラス群が含まれます。
:::

[^output]: 正確には、`schema.prisma`の`output`に指定したパス。

### 3.2. クエリ実行

　クライアントを生成できたので、JSからDBクエリを実行してみます。ドキュメントに書かれているコードをコピペしてきました。[^import]

[^import]: 今回はESModulesを使用しているので、import文に変更しました。

```js:src/server.mjs
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
    const allUsers = await prisma.user.findMany();
    console.log(allUsers);
}

main()
    .then(async () => {
        await prisma.$disconnect();
    })
    .catch(async (e) => {
        console.error(e);
        await prisma.$disconnect();
        process.exit(1);
    });
```

　これで`src/server.mjs`を実行すれば[^container]、DBのUserテーブルの全レコードを取得することが<font color="red">できません</font>。

```bash
$ node src/server.mjs
```

　実行すると、以下のようなエラーが出ました。

<details><summary>実行結果</summary>

```bash
$ node src/server.mjs

/app/node_modules/.prisma/client/default.js:43
    throw new Error('@prisma/client did not initialize yet. Please run "prisma generate" 
and try to import it again.');
          ^

Error: @prisma/client did not initialize yet. Please run "prisma generate" and try to 
import it again.
    at new PrismaClient (/app/node_modules/.prisma/client/default.js:43:11)
    at file:///app/src/server.mjs:18:16
    at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)

Node.js v23.11.0
```

</details>

#### 罠2: 出力ディレクトリと importパスの相違

> エラー：@prisma/clientがまだ初期化されていません。"prisma generate"を実行し、再度インポートしてください。

　なんか言われてますが、先ほど書いた通り`prisma generate`は実行してあります。

　考えてみれば当然なのですが、クライアントを`generated/prisma/`に生成したにもかかわらず、`@prisma/client`からインポートしようとしていることが問題です。ドキュメントに沿って導入しようとしたのにうまくいかないという罠です。
　ではなぜ`@prisma/client`からインポートしようとしているかというと、`schema.prisma`の`output`を省略した際のデフォルト設定がそこだからですね。

　結局のところどうすればいいのかというと：
- `@prisma/client`からインポートしたいなら、`schema.prisma`の`output`を削除する
- `output`で指定した場所からインポートしたいなら、そのパスを指定してインポートする（`../generated/prisma/index.js`）

　今回は後者を選びました。
```diff_javascript:src/server.mjs
- import { PrismaClient } from "@prisma/client";
+ import { PrismaClient } from "../generated/prisma/index.js";

  const prisma = new PrismaClient();

  async function main() {
      const allUsers = await prisma.user.findMany();
      console.log(allUsers);
  }

  main()
      .then(async () => {
          await prisma.$disconnect();
      })
      .catch(async (e) => {
          console.error(e);
          await prisma.$disconnect();
          process.exit(1);
      });
```

　今度は無事に成功しました。
```bash
$ node src/server.mjs
[]
```

　ちなみに、`output`を設定すると何がいいのかGeminiさんに聞いたので、参考までに載せておきます。

<details><summary>Gemini回答</summary>

**Prismaのoutput設定について**

<p>Prismaの<code>output = "../generated/prisma"</code>設定は、Prisma Clientの生成場所を指定するために使用されます。この設定の主な理由は以下の通りです。</p>

1. コードの整理と管理
    <ul>
      <li>生成されたPrisma Clientコードをプロジェクト内の特定のディレクトリに分離することで、コードベースを整理し、管理しやすくします。</li>
      <li><code>node_modules</code>ディレクトリに直接生成するのではなく、独立したディレクトリに生成することで、依存関係の管理を簡素化し、不要なファイルの混在を防ぎます。</li>
    </ul>
2. カスタマイズと柔軟性
    <ul>
      <li>生成場所をカスタマイズすることで、プロジェクトの構造や開発ワークフローに合わせてPrisma Clientを配置できます。</li>
      <li>例えば、複数のPrismaスキーマを使用する場合、それぞれのスキーマごとに異なる出力ディレクトリを指定することで、コードをより細かく管理できます。</li>
    </ul>
3. バージョン管理とクリーンなビルド
    <ul>
      <li>生成されたコードをバージョン管理対象から除外することで、リポジトリのサイズを削減し、不要な変更の追跡を防ぎます。</li>
      <li>クリーンなビルド環境を維持するために、生成されたコードをビルドプロセスで簡単に削除および再生成できるようにします。</li>
    </ul>

**具体的な例**
<ul>
  <li><code>../generated/prisma</code>のように、プロジェクトのルートディレクトリから相対パスで指定することで、生成場所を柔軟に設定できます。</li>
  <li>これにより、例えば<code>src/lib/prisma</code>のように、プロジェクト内の特定のディレクトリにPrisma Clientを生成することも可能です。</li>
</ul>

**補足**
<ul>
  <li><code>output</code>設定は、<code>prisma.schema</code>ファイル内でgeneratorブロックの一部として定義されます。</li>
  <li>Prisma Clientの生成場所を変更した場合、アプリケーション内のPrisma Clientのインポートパスも更新する必要があります。</li>
  <li>Prismaの公式ドキュメントでは、この設定に関する詳細な情報やその他の設定オプションについても解説されています。</li>
</ul>

</details>

---

　これでPrismaを導入して利用できるようになりました。あとはクエリの仕方などを学んでいけば良いです。

# さいごに

　導入や環境構築は、まあ根気強くやればどんなものもできるんでしょうけど、それはそれとしてこういうことがあったら普通に嫌な気持ちになりますね。これが積み重なって勉強のやる気が無くなる可能性があるのが普通につらい。会社だと先輩なんかに聞いて解決したり一緒に悩んだりできるので、初心者とかは会社で勉強した方がすごく楽だろうなぁと思いました。