1
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】Prismaを使ってローカル(sqlite)と本番(postgresql)でDBを使い分ける

Last updated at Posted at 2025-10-20

やりたいこと

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 サンプル)
ts-study-record-aws/backend/server.ts
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

ts-study-record-aws/backend/src/db.ts
// generatedディレクトリは.gitignoreに追加するため本番と分ける必要はない
import { PrismaClient } from '../generated/prisma/index.js';

const prisma = new PrismaClient();

export default prisma;
ts-study-record-aws/backend/prisma/sqlite/schema.sqlite.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())
}
ts-study-record-aws//backend/.env
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
動作確認

image.png

本番 / postgresql

AWS構成イメージ
ts-study-record-aws/backend/prisma/postgres/schema.postgres.prisma
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())
}
ts-study-record-aws/backend/src/db.ts
import { PrismaClient } from '../generated/prisma/index.js';

const prisma = new PrismaClient();

export default prisma;
/backend/.env
// ローカルもマイグレーションファイル作成に使うため、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

1
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
1
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?