やりたいこと
Prismaを使い、ローカル開発ではSQLite、本番環境ではPostgreSQLを使い分ける構成を紹介します。
開発でSQLiteを使う理由
- サーバ不要、ファイル1つで即動く
- セットアップが超速、環境構築が楽
- スキーマ変更やマイグレーションの試験が手軽
- チーム全員がローカルで同じ状態を再現しやすい
本番でPostgreSQLを使う理由
-
同時アクセスに強い
SQLiteは「1人が書き込み中は他の書き込みをブロック」するが、PostgreSQLは同時に数百・数千のクエリを処理できる - RDSなどでスケール・運用がしやすい
- PrismaはPostgres向け最適化が豊富
- 本番運用の信頼性・安定性が高い
Prismaとは
Node/TypeScript向けのモダンなORM(データベース操作を楽にするライブラリ)。
使うとSQLを直接大量に書かずに型安全な方法でDB操作ができる。
- Prisma Client:TypeScript用の自動生成クライアント(コードから DB 操作)
- Prisma Migrate:スキーマ変更をSQLマイグレーションとして管理・適用する仕組み
- Prisma Studio:ブラウザでDBを覗けるGUI
- schema.prisma:DB設計書(datasource / generator / modelを定義するファイル)
前提
本構成では、frontend
ディレクトリの Reactアプリから、backend
ディレクトリのサーバーAPI(Express + Prisma)を呼び出す形になっています。
server.ts(Express + Prisma サンプル)
import type { Server } from 'http';
import express from 'express';
import prisma from './src/db.ts';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
app.get('/records', async (req, res) => {
try {
const rows = await prisma.studyRecord.findMany({ orderBy: { created_at: 'asc' } });
res.json(rows);
} catch (err) {
console.error('GET /records error:', err);
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
});
app.post('/records', async (req, res) => {
try {
const { title, time } = req.body;
if (!title || typeof time !== 'number') {
return res.status(400).json({ error: 'Invalid request body' });
}
const newRecord = await prisma.studyRecord.create({ data: { title, time } });
res.status(201).json(newRecord);
} catch (err) {
console.error('POST /records error:', err);
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
});
app.put('/records/:id', async (req, res) => {
try {
const { id } = req.params;
const { title, time } = req.body;
if (!title || typeof time !== 'number') {
return res.status(400).json({ error: 'Invalid request body' });
}
const updatedRecord = await prisma.studyRecord.update({
where: { id },
data: { title, time }
});
res.json(updatedRecord);
} catch (err) {
console.error('PUT /records/:id error:', err);
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
});
app.delete('/records/:id', async (req, res) => {
try {
const { id } = req.params;
if (!id) return res.status(400).json({ error: 'Missing id' });
await prisma.studyRecord.delete({ where: { id } });
res.status(204).send();
} catch (err) {
console.error('DELETE /records/:id error:', err);
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
});
let server: Server | null = null;
let keepalive: NodeJS.Timeout | null = null;
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
prisma.$connect()
.then(() => {
keepalive = setInterval(async () => {
try {
await prisma.$queryRaw`SELECT 1`;
} catch (err) {
console.error('Database keepalive failed:', err);
}
}, 30000);
server = app.listen(4000, '0.0.0.0', () => {
console.log('Server running on port 4000');
});
})
.catch(err => {
console.error('Prisma接続失敗', err);
process.exit(1);
});
const shutdown = async (signal: string, code = 0) => {
if (server && typeof server.close === 'function') {
const s = server;
await new Promise<void>((resolve) => {
s.close(() => resolve());
setTimeout(() => resolve(), 10000);
});
}
if (keepalive) clearInterval(keepalive);
try {
await prisma.$disconnect();
} catch (e) {
console.error('Error disconnecting Prisma:', e);
}
process.exit(code);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('uncaughtException', (err) => {
console.error('uncaughtException:', err);
void shutdown('uncaughtException', 1);
});
process.on('unhandledRejection', (err) => {
console.error('unhandledRejection:', err);
void shutdown('unhandledRejection', 1);
});
解決方法
ローカル / sqlite
// generatedディレクトリは.gitignoreに追加するため本番と分ける必要はない
import { PrismaClient } from '../generated/prisma/index.js';
const prisma = new PrismaClient();
export default prisma;
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
//Prisma の generator 設定で出力先指定
//schema.prisma を基準にして相対パスで指定され、Prisma Clientの生成物(JS/TS)がそのディレクトリに書き出される
//例えば schema.prisma が backend/prisma/sqlite/schema.sqlite.prismaにあるなら、生成先は backend/generated/prismaになる
//generatedはgitignore追加する
generator client {
provider = "prisma-client-js"
output = "../../generated/prisma" // ※階層注意
}
model StudyRecord {
id String @id @default(uuid())
title String
time Int
created_at DateTime @default(now())
}
DATABASE_URL=file:./prisma/dev.db
コマンド例
# Prisma Clientを生成
npx prisma generate --schema=prisma/sqlite/schema.sqlite.prisma
# マイグレーションを作成・適用するとき(履歴を残す)
npx prisma migrate dev --schema=prisma/sqlite/schema.sqlite.prisma --name <migration名>
# Prisma Studioを開くとき
npx prisma studio --schema=prisma/sqlite/schema.sqlite.prisma
動作確認
本番 / postgresql
AWS構成イメージ
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
//Prisma の generator 設定で出力先指定
//schema.prisma を基準にして相対パスで指定され、Prisma Client の生成物(JS/TS)がそのディレクトリに書き出される
//例えば schema.prisma が backend/prisma/sqlite/schema.sqlite.prisma にあるなら、生成先は backend/generated/prisma になる
//generatedはgitignore追加する
generator client {
provider = "prisma-client-js"
output = "../../generated/prisma" // ※階層注意
}
model StudyRecord {
id String @id @default(uuid())
title String
time Int
created_at DateTime @default(now())
}
// testで追加
model TestModel {
id String @id @default(uuid())
name String
createdAt DateTime @default(now())
}
import { PrismaClient } from '../generated/prisma/index.js';
const prisma = new PrismaClient();
export default prisma;
// ローカルもマイグレーションファイル作成に使うため、postgresqlのDATABASE_URLにする
DATABASE_URL="postgresql://<USER>:<REDACTED>@<HOST>:5432/<DB>"
コマンド例
# ローカルで対応
# 本番DBに直接マイグレーションせず、ローカルでファイル生成
# セキュリティグループでbastionとRDSの5432ポート接続を許可
# ローカルから踏み台サーバー経由でRDSにトンネル接続
# トンネル接続する場合、AWS側でセキュリティグループのインバウンド/アウトバウンド設定で
# bastionサーバーとRDSの5432ポート接続を許可しておく必要あり
ssh -L 5432:<RDSエンドポイント>:5432 ec2-user@<BASTION_HOST> -N
# ※このコマンドは開いたまま、別ターミナルで実行
# マイグレーションファイルを作成(DBに即適用したくない場合は--create-only推奨)
npx prisma migrate dev --schema=prisma/postgres/schema.postgres.prisma --name <migration名> --create-only
# 生成されたマイグレーションファイルをGitにプッシュ
git add prisma/migrations/
git commit -m "Add <migration名> migration"
git push
# 本番環境で対応
# migrationファイルをpull
git pull
# マイグレーションを本番用に非対話で適用(既に作成・コミット済みの prisma/migrations を適用)
npx prisma migrate deploy --schema=prisma/postgres/schema.postgres.prisma
# Prisma Client を生成(アプリ起動前に実行)
npx prisma generate --schema=prisma/postgres/schema.postgres.prisma
動作確認
本番環境でPostgreSQLのDBに繋ぎ、追加したTestModelを取得できることを確認。
postgres=> \d "TestModel"
Table "public.TestModel"
Column | Type | Collation | Nullable | Default
-----------+--------------------------------+-----------+----------+-------------------
id | text | | not null |
name | text | | not null |
createdAt | timestamp(3) without time zone | | not null | CURRENT_TIMESTAMP
Indexes:
"TestModel_pkey" PRIMARY KEY, btree (id)
終わりに
schema.postgres.prismaやschema.sqlite.prismaのgenerator clientのoutput階層が違っていて正しく動かなかったり、AWSのセキュリティグループのインバウンド・アウトバウンド設定がうまくできていなくて苦労しました。
参考情報
Getting started with Prisma Migrate
https://www.prisma.io/docs/orm/prisma-migrate/getting-started
Managing Prisma ORM environment variables and settings
https://www.prisma.io/docs/orm/more/development-environment/environment-variables
Connect your database using TypeScript and PostgreSQL
https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/connect-your-database-typescript-postgresql