はじめに
Webアプリ初心者で、DB操作のためにPrismaを使ってみようと思って導入していたところ、罠にはめられて夜神ライトになったというお話を、Prismaの導入方法と合わせて書いていきます。
目次
環境
各バージョン
- Node.js v23.11.0
- MySQL2 v3.14.0
- Prisma v6.5.0
- Docker v28.0.4
- サーバー用、DB用コンテナを用意
導入前の状態
必要最低限のものだけ載せています。
- JSを使用していますが、TSでも変わりません
- Dockerを使用していますが、他の状況でも変わらないと思います
ディレクトリ構成
.
├── db/
│ ├── .env
│ └── conf.d/
│ └── my.cnf
├── server/
│ ├── node_modules/
│ ├── src/
│ │ └── server.mjs
│ ├── Dockerfile
│ ├── package.json
│ └── package-lock.json
└── docker-compose.yaml
各ファイルの内容
MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=password
MYSQL_USER=dbuser
MYSQL_PASSWORD=password
MYSQL_DATABASE=hoge_database
{
"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",
}
}
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install -g npm
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
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:
本編: Prisma導入
基本は以下の公式ドキュメント通りに進めていきましたが、この通りにやるだけだと進めなかったので、解決方法を入れつつ書いていきます。
1. インストールと初期設定
1.1. インストール
server
ディレクトリ内で作業していきます。まずはインストールから。
$ cd server
$ npm install prisma --save-dev
以下のコマンドでエラーが出なければ、インストール成功です。
$ npx prisma
1.2. 初期設定
次のコマンドで、Prismaスキーマファイルを作成します。
$ npx prisma init --datasource-provider mysql --output ../generated/prisma
実行すると、以下のファイルが生成されます。
server/
├── ...
├── .env
└── prisma/
└── schema.prisma
# 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"
// 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
の内容に合わせて、(環境変数を使いまわす方法がわからなかったので)べた書きしました。
# "DBの種類://ユーザ名:パスワード@接続先ホスト:ポート番号/DB名"
DATABASE_URL="mysql://dbuser:password@db:3306/hoge_database"
接続先ホストについて
今回は、docker-composeを用いて環境構築をしているため、サービス名の`db`を指定してます。DBがどこにあるのかに合わせて指定する必要があります。- ローカル環境:
localhost
- リモート環境:IPアドレスなど
- Docker Networkで接続されたコンテナ:コンテナ名
初期設定はこれで完了しました。公式ドキュメントに忠実に従いましたが、既に罠が仕込まれています。発動するのはまだ少し先です。
2. Prisma Migrationでテーブル作成
次は、DBに接続してテーブルを作成します。
2.1. スキーマ定義
Prismaでは、schema.prisma
にテーブルモデルを記述し、それを実際のDBスキーマに反映します(Prisma Migration)。ドキュメントの例にあるモデルを追加しました。
テーブルモデル追加
// ...(省略)
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?
}
2.2. テーブル作成
テーブルモデルができたので、DBにテーブルを追加していきます。(私はDockerを使用していますが、関係ない箇所の操作は省略します。)
次のコマンドを実行する1ことで、接続先DBにテーブルを作成することができません。
$ npx prisma migrate dev --name init
dev
:開発環境用のコマンド
--name
:モデル変更の説明用コメントを付けられる
実行すると、以下のようなエラーが出ました。
実行結果
$ 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`
罠1: 権限不足
Prisma Migrateはシャドウデータベースを作成できませんでした。データベース ユーザーにデータベースを作成する権限があることを確認してください。シャドウデータベース(および回避策)については、https://pris.ly/d/migrate-shadow を参照してください。
ユーザーdbuser
はデータベースhoge_database
へのアクセスを拒否されました。
記載されているURLにアクセスしてみました。
prisma migration dev
コマンドを実行すると、
- シャドウデータベースを使用し、スキーマ変更履歴から現在の状態を生成する
- 生成したものと実態を比較し、一致しない場合はスキーマドリフトを報告する
- スキーマドリフトが検出されなかった場合、今回の変更をDBに適用する
という流れをとるようです。このシャドウデータベースを操作するための権限がdbuser
に無いため、失敗してしまいました。
同じページに、
MySQL/MariaDB データベースユーザーには CREATE, ALTER, DROP, REFERENCES ON *.* 権限が必要です
と書いてあるので、MySQLのroot
ユーザから権限を与えます。1
$ mysql -u root -p
mysql> GRANT CREATE, ALTER, DROP, REFERENCES ON *.* TO `dbuser`@`%`;
権限を与えたので、もう一度コマンドを実行してみます。
実行結果
$ 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
今度は成功しました。
DBを直接確認すると、しっかりテーブルが作成されていることが確認できました。
テーブル情報確認
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)
成功すると、以下のファイルが生成されました。
server/
├── ...
├── .env
├── prisma/
│ ├── schema.prisma
│ └── migrations/ --------------- # New
│ ├── 20250414164608_init/ -- # New
│ │ └── migration.sql ----- # New
│ └── migration_lock.toml --- # New
└── generated/ -------------------- # New
└── prisma/ ------------------- # New
└── ...
migrations/
の中では、スキーマの変更履歴を確認することができます。ディレクトリ名(20250414164608_init
)で変更日時2とコメントを、migration.sql
で実行されたSQL文を確認できます。
(generated/prisma/
については、次の章で説明します。)
テーブルの作成については、これで可能になりました。スキーマを変更する際には、この章の手順を繰り返すことになります。
3. JavaScriptコードから、DBクエリを実行
次は、Prismaを使用してJSからDBクエリを実行できるようにします。
3.1. 準備
JSで使用できるようにするためには、Prisma Clientをインストールし、prisma generate
コマンドを実行する必要があります。しかし、実は前の章のprisma migrate
コマンドを実行した際に、これらの工程も一緒に実行されています。genetated/prisma/
は、prisma generate
コマンドによって生成されたものです。
ディレクトリが生成されていない場合
以下のコマンドを実行します。
$ npm install @prisma/client
$ npx prisma generate
generated/prisma/
3には、JS/TS用のORMクライアント(Prisma Client)が生成されます。DB接続・クエリにかかわるものや、DBスキーマに基づくモデルクラス群が含まれます。
3.2. クエリ実行
クライアントを生成できたので、JSからDBクエリを実行してみます。ドキュメントに書かれているコードをコピペしてきました。4
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
を実行すれば1、DBのUserテーブルの全レコードを取得することができません。
$ node src/server.mjs
実行すると、以下のようなエラーが出ました。
実行結果
$ 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
罠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
)
今回は後者を選びました。
- 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);
});
今度は無事に成功しました。
$ node src/server.mjs
[]
ちなみに、output
を設定すると何がいいのかGeminiさんに聞いたので、参考までに載せておきます。
Gemini回答
Prismaのoutput設定について
Prismaのoutput = "../generated/prisma"
設定は、Prisma Clientの生成場所を指定するために使用されます。この設定の主な理由は以下の通りです。
- コードの整理と管理
- 生成されたPrisma Clientコードをプロジェクト内の特定のディレクトリに分離することで、コードベースを整理し、管理しやすくします。
-
node_modules
ディレクトリに直接生成するのではなく、独立したディレクトリに生成することで、依存関係の管理を簡素化し、不要なファイルの混在を防ぎます。
- カスタマイズと柔軟性
- 生成場所をカスタマイズすることで、プロジェクトの構造や開発ワークフローに合わせてPrisma Clientを配置できます。
- 例えば、複数のPrismaスキーマを使用する場合、それぞれのスキーマごとに異なる出力ディレクトリを指定することで、コードをより細かく管理できます。
- バージョン管理とクリーンなビルド
- 生成されたコードをバージョン管理対象から除外することで、リポジトリのサイズを削減し、不要な変更の追跡を防ぎます。
- クリーンなビルド環境を維持するために、生成されたコードをビルドプロセスで簡単に削除および再生成できるようにします。
具体的な例
-
../generated/prisma
のように、プロジェクトのルートディレクトリから相対パスで指定することで、生成場所を柔軟に設定できます。 - これにより、例えば
src/lib/prisma
のように、プロジェクト内の特定のディレクトリにPrisma Clientを生成することも可能です。
補足
-
output
設定は、prisma.schema
ファイル内でgeneratorブロックの一部として定義されます。 - Prisma Clientの生成場所を変更した場合、アプリケーション内のPrisma Clientのインポートパスも更新する必要があります。
- Prismaの公式ドキュメントでは、この設定に関する詳細な情報やその他の設定オプションについても解説されています。
これでPrismaを導入して利用できるようになりました。あとはクエリの仕方などを学んでいけば良いです。
さいごに
導入や環境構築は、まあ根気強くやればどんなものもできるんでしょうけど、それはそれとしてこういうことがあったら普通に嫌な気持ちになりますね。これが積み重なって勉強のやる気が無くなる可能性があるのが普通につらい。会社だと先輩なんかに聞いて解決したり一緒に悩んだりできるので、初心者とかは会社で勉強した方がすごく楽だろうなぁと思いました。