2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-16

はじめに

 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

各ファイルの内容

db/.env
MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=password
MYSQL_USER=dbuser
MYSQL_PASSWORD=password
MYSQL_DATABASE=hoge_database
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",
  }
}
server/dockerfile
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install -g npm
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
./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:

本編: 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
.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"
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へ接続できるようにするため、.envDATABASE_URLを設定します。db/.envの内容に合わせて、(環境変数を使いまわす方法がわからなかったので)べた書きしました。

.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)。ドキュメントの例にあるモデルを追加しました。

テーブルモデル追加
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?
}

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コマンドを実行すると、

  1. シャドウデータベースを使用し、スキーマ変更履歴から現在の状態を生成する
  2. 生成したものと実態を比較し、一致しない場合はスキーマドリフトを報告する
  3. スキーマドリフトが検出されなかった場合、今回の変更を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

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を実行すれば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.prismaoutputを省略した際のデフォルト設定がそこだからですね。

 結局のところどうすればいいのかというと:

  • @prisma/clientからインポートしたいなら、schema.prismaoutputを削除する
  • outputで指定した場所からインポートしたいなら、そのパスを指定してインポートする(../generated/prisma/index.js

 今回は後者を選びました。

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);
      });

 今度は無事に成功しました。

$ node src/server.mjs
[]

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

Gemini回答

Prismaのoutput設定について

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

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

具体的な例

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

補足

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

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

さいごに

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

  1. Dockerの場合は、コンテナ内で実行する。 2 3

  2. 実行環境のタイムゾーンが適用される。

  3. 正確には、schema.prismaoutputに指定したパス。

  4. 今回はESModulesを使用しているので、import文に変更しました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?