1. なぜ生の SQL や古い Query Builder ではなく Prisma ORM なのか?

酒本先輩!
レイヤードアーキテクチャと DI の設計が完了して、メモリ上でのカート機能は完璧になりました。
でも、このままだとサーバーを再起動したらデータが全部消えちゃいますよね。
いよいよ本格的なデータベース(PostgreSQL)を繋ぎたいです!
でも僕の PC、PostgreSQL とかインストールしてないんですよね…

大丈夫よ!今は Docker を使えばローカル環境を汚さずに数分で PostgreSQL を準備できるわ。
説得力を持って説明すると、実務の Node.js 開発において現在のモダンなデータベースアクセスの標準は圧倒的に Prisma ORM よ。
特に最新の Prisma 7 では、Rust-free の新エンジンとドライバーアダプター構造によってパフォーマンスと型安全性がさらに進化しているわ。
採用すべき理由は以下の3点にあるわ。
- 完全な型安全性(Type Safety): スキーマ定義ファイル(
schema.prisma)から自動生成される TypeScript の型が強力で、DBのクエリ結果と型が完全に一致する。 - 直感的なクエリ API:
prisma.user.findMany({ include: { posts: true } })のように、SQLを書かなくても直感的なメソッドチェーンでリレーションを扱える。 - 安全なマイグレーション管理:
prisma migrateコマンドにより、データベースのスキーマ変更履歴(バージョン管理)を安全にチーム共有できる。
【今回のデータフローと接続構成(Prisma 7)】
1. 開発PC(ローカル環境)
├─ schema.prisma(スキーマ定義)
└─ prisma7.config.ts(init 時に自動生成される設定ファイル)
│
│ (npx prisma generate) ※ prisma7.config.ts を自動検出
▼
2. 生成された Prisma Client (src/generated/prisma/client) + pg Adapter
│
▼
3. Docker コンテナ(PostgreSQL:17 / 実データベース)
2. 【導入ステップ】Docker Compose で PostgreSQL を起動する

まずは PC に PostgreSQL を入れずに、Docker でデータベースコンテナを立ち上げましょう。
1. compose.yaml をプロジェクトルートに作成
# compose.yaml
services:
postgres:
image: postgres:17-alpine
container_name: dev-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgrespassword
POSTGRES_DB: my_layered_db
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
2. データベースコンテナの起動と状態確認
docker compose up -d
docker ps
確認:
docker psを実行してdev-postgresの STATUS がUpになっていれば DB の準備は完了です。
3. 実務標準の環境構築と Prisma 7 の初期セットアップ
1. パッケージのインストール
npm install @prisma/client @prisma/adapter-pg pg
npm install -D prisma @types/pg dotenv
2. Prisma の初期化(設定ファイルと .env の自動生成)
npx prisma init
実行後、プロジェクトルートに prisma/schema.prisma 、prisma7.config.ts 、.env が自動生成されます。
3. .env の接続情報を確認・更新
自動生成された .env 内の DATABASE_URL を PostgreSQL の接続情報に合わせます。
# .env
DATABASE_URL="postgresql://postgres:postgrespassword@localhost:5432/my_layered_db?schema=public"
4. 自動生成された prisma7.config.ts の確認
自動生成された prisma7.config.ts は以下のような設定になっています。
// prisma7.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
url: env("DATABASE_URL"),
},
});
4. スキーマ設計:schema.prisma の記述とリレーション
prisma/schema.prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client" // Prisma 7 の新しい Rust-free クライアント
output = "../src/generated/prisma/client" // 生成コードの出力パス指定
}
datasource db {
provider = "postgresql" // 接続URLは config 側で参照
}
// カートモデル
model Cart {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items CartItem[] // リレーション(1つのカートに複数のアイテム)
}
// カート内商品モデル
model CartItem {
id Int @id @default(autoincrement())
productName String @db.VarChar(255)
price Int
quantity Int @default(1)
couponCode String? @db.VarChar(50)
cartId Int // 外部キー
cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
5. マイグレーションの実行と Prisma Client の生成

prisma7.config.ts は CLI によって自動検出されるから、オプションを指定せずにそのままでマイグレーションを実行できるわ。
# マイグレーション実行(prisma7.config.ts が自動ロードされます)
npx prisma migrate dev --name init_cart_schema
6. 実践コード:Prisma 7 を組み込んだリポジトリ層の実装
src/repositories/prisma/prisma-cart.repository.ts
// src/repositories/prisma/prisma-cart.repository.ts
import { PrismaClient } from '../../generated/prisma/client/index.js';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
import { ICartRepository } from '../interfaces/cart.repository.interface.js';
import { CartItem } from '../../types/cart.type.js';
export class PrismaCartRepository implements ICartRepository {
private prisma: PrismaClient;
constructor() {
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL || "postgresql://postgres:postgrespassword@localhost:5432/my_layered_db?schema=public",
});
const adapter = new PrismaPg(pool);
this.prisma = new PrismaClient({ adapter });
}
private getOrCreateDefaultCartId = async (): Promise<number> => {
let cart = await this.prisma.cart.findFirst();
if (!cart) {
cart = await this.prisma.cart.create({ data: {} });
}
return cart.id;
};
findAll = async (): Promise<CartItem[]> => {
const cartId = await this.getOrCreateDefaultCartId();
const items = await this.prisma.cartItem.findMany({
where: { cartId },
orderBy: { id: 'asc' },
});
return items.map((item) => ({
id: item.id,
productName: item.productName,
price: item.price,
quantity: item.quantity,
}));
};
findById = async (id: number): Promise<CartItem | undefined> => {
const item = await this.prisma.cartItem.findUnique({
where: { id },
});
if (!item) return undefined;
return {
id: item.id,
productName: item.productName,
price: item.price,
quantity: item.quantity,
};
};
create = async (data: Omit<CartItem, 'id'>): Promise<CartItem> => {
const cartId = await this.getOrCreateDefaultCartId();
const newItem = await this.prisma.cartItem.create({
data: {
productName: data.productName,
price: data.price,
quantity: data.quantity,
cartId: cartId,
},
});
return {
id: newItem.id,
productName: newItem.productName,
price: newItem.price,
quantity: newItem.quantity,
};
};
updateQuantity = async (id: number, quantity: number): Promise<CartItem | undefined> => {
try {
const updated = await this.prisma.cartItem.update({
where: { id },
data: { quantity },
});
return {
id: updated.id,
productName: updated.productName,
price: updated.price,
quantity: updated.quantity,
};
} catch {
return undefined;
}
};
delete = async (id: number): Promise<boolean> => {
try {
await this.prisma.cartItem.delete({
where: { id },
});
return true;
} catch {
return false;
}
};
}
7. DI(依存性の注入)の切り替えとルーティング修正
src/routes/cart.route.ts
// src/routes/cart.route.ts
import { Router } from 'express';
import { CartController } from '../controllers/cart.controller.js';
import { CartService } from '../services/cart.service.js';
import { PrismaCartRepository } from '../repositories/prisma/prisma-cart.repository.js';
import { validateRequest } from '../middlewares/validate.middleware.js';
import { createCartItemSchema, updateCartItemSchema } from '../schemas/cart.schema.js';
const cartRepository = new PrismaCartRepository();
const cartService = new CartService(cartRepository);
const cartController = new CartController(cartService);
const router = Router();
router.get('/items', cartController.getSummary.bind(cartController));
router.post('/items', validateRequest({ body: createCartItemSchema }), cartController.createItem.bind(cartController));
router.patch('/items/:id', validateRequest({ body: updateCartItemSchema }), cartController.updateQuantity.bind(cartController));
router.delete('/items/:id', cartController.deleteItem.bind(cartController));
export default router;
8. curl による動作確認手順
# データ追加
curl -i -X POST http://localhost:3000/api/cart/items \
-H "Content-Type: application/json" \
-d '{"productName": "メカニカルキーボード", "price": 18500, "quantity": 1}'
# データ取得確認
curl -i -X GET http://localhost:3000/api/cart/items
本日のまとめ
- Docker Compose による環境構築:
compose.yaml1つで PostgreSQL を立ち上げた。 - Prisma 7 初期化と構成:
npx prisma initで自動生成されたprisma7.config.tsと.envを使い、npx prisma migrate devの実行により自動ロードを行ってマイグレーションと型生成を完了した。 - DI による切り替え: インターフェースに従い
PrismaCartRepositoryを作成し、上位レイヤーを変更せずに永続化対応を完了した。
次回:第14回「Prisma Client による型安全な CRUD 操作と高度なリレーション・トランザクション処理」へ続く

