Prismaデータベース
MegaShopは常にモックデータを使用していました - 商品データはJavaScript配列に格納され, 再起動すると失われます。Charlieは本物のデータベースを必要としています。Bobが書いた生のSQLはエラーが発生しやすく, 型安全性に欠けています。Prisma ORMは型安全なデータベースクエリを提供し, 1行のコードでTypeScript型を生成します。
1. 学ぶ内容
- Prismaのインストールと初期化:schema.prisma + generate + migrate
- データモデル設計:Product, Category, User, Order, CartItemテーブル間のリレーション
- Nuxt統合:server/utils/prisma.tsシングルトン接続
- CRUD操作:findMany/create/update/delete + トランザクション + 集計
- MegaShop:数百万商品のページネーション検索 + 注文トランザクション
2. アーキテクトのリアルストーリー
(1) ペインポイント:モックデータは永続化できない
BobがMegaShopの開発サーバーを再起動すると, すべての商品データが消えました。Aliceが追加したすべての商品や注文が消えてしまいました。さらに悪いことに, 本番環境にはデータベースがなく, 数百万商品のデータを保存する場所がありませんでした。
(2) Prisma ORMソリューション
Prismaは型安全なデータベース操作を提供し, TypeScript型を自動生成します:
TYPESCRIPT
// 型安全なクエリ - 生のSQLなし
const products = await prisma.product.findMany({
where: { category: { slug: 'electronics' } },
take: 20,
skip: (page - 1) * 20
})
(3) 利点:型安全性 + データ永続化
商品データは永続的に保存され, APIクエリは完全な型推論を備えています - Bobはもうフィールド名のスペルミスをすることはなく, Aliceの注文は再起動後も残ります。
3. Prismaのインストールと初期化
(1) ▶サンプル:Prismaのインストール
BASH
npm install prisma @prisma/client
npx prisma init
出力:
TEXT
# コマンド実行成功
(2) ▶サンプル:Prismaスキーマ
PRISMA
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Product {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal @db.Decimal(10, 2)
image String?
inStock Boolean @default(true)
rating Float @default(0)
reviewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
cartItems CartItem[]
@@index([categoryId])
@@index([slug])
@@index([price])
}
model Category {
id Int @id @default(autoincrement())
name String
slug String @unique
parentId Int?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
children Category[] @relation("CategoryTree")
products Product[]
@@index([slug])
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String?
avatar String?
provider String @default("email")
role Role @default(CUSTOMER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
cartItems CartItem[]
@@index([email])
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
total Decimal @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
@@index([userId])
@@index([status])
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int
order Order @relation(fields: [orderId], references: [id])
productId Int
product Product @relation(fields: [productId], references: [id])
quantity Int
price Decimal @db.Decimal(10, 2)
@@index([orderId])
}
model CartItem {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
productId Int
product Product @relation(fields: [productId], references: [id])
quantity Int @default(1)
@@unique([userId, productId])
}
enum Role {
CUSTOMER
ADMIN
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
出力:
TEXT
// 実行成功
(1) MegaShop ER図
erDiagram
Product ||--o{ OrderItem : "含まれる"
Product ||--o{ CartItem : "追加される"
Product }o--|| Category : "属する"
Category ||--o{ Category : "親子"
User ||--o{ Order : "注文する"
User ||--o{ CartItem : "持つ"
Order ||--o{ OrderItem : "含む"
4. Nuxt統合
(1) ▶サンプル:Prismaシングルトン接続
TYPESCRIPT
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
// シングルトンパターン - 開発時の複数インスタンス生成を防止
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
出力:
TEXT
// 実行成功
(2) ▶サンプル:Nitro事前ビルドPrismaクライアント
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
// ビルド前にPrismaクライアントを生成
externals: {
inline: ['.prisma/client']
}
},
hooks: {
'build:before': async () => {
const { execSync } = await import('child_process')
execSync('npx prisma generate')
}
}
})
出力:
TEXT
// 実行成功
5. CRUD操作
(1) ▶サンプル:商品一覧のページネーションクエリ
TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 20
const category = query.category as string
const search = query.search as string
const minPrice = Number(query.minPrice) || 0
const maxPrice = Number(query.maxPrice) || Infinity
const where = {
AND: [
category ? { category: { slug: category } } : {},
search ? { name: { contains: search, mode: 'insensitive' as const } } : {},
{ price: { gte: minPrice } },
maxPrice < Infinity ? { price: { lte: maxPrice } } : {}
]
}
const [items, total] = await Promise.all([
prisma.product.findMany({
where,
skip: (page - 1) * limit,
take: limit,
include: { category: { select: { name: true, slug: true } } },
orderBy: { createdAt: 'desc' }
}),
prisma.product.count({ where })
])
return { items, total, page, limit }
})
出力:
TEXT
// 実行成功
(2) ▶サンプル:商品詳細クエリ
TYPESCRIPT
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const product = await prisma.product.findUnique({
where: { id },
include: {
category: { select: { name: true, slug: true } }
}
})
if (!product) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
return product
})
出力:
TEXT
// 実行成功
(3) ▶サンプル:注文の作成 (トランザクション)
TYPESCRIPT
// server/api/orders/index.post.ts
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
const { items } = await readBody(event)
// トランザクション:注文作成 + 在庫更新 + カートクリア
const order = await prisma.$transaction(async (tx) => {
// 合計金額の計算
let total = 0
const orderItems = []
for (const item of items) {
const product = await tx.product.findUnique({ where: { id: item.productId } })
if (!product || !product.inStock) {
throw createError({ statusCode: 400, message: `Product ${item.productId} unavailable` })
}
total += Number(product.price) * item.quantity
orderItems.push({
productId: product.id,
quantity: item.quantity,
price: product.price
})
}
// 注文の作成
const newOrder = await tx.order.create({
data: {
userId,
total,
items: { create: orderItems }
},
include: { items: { include: { product: true } } }
})
// ユーザーのカートをクリア
await tx.cartItem.deleteMany({ where: { userId } })
return newOrder
})
return { order, message: 'Order created successfully' }
})
出力:
TEXT
// 実行成功
(4) ▶サンプル:集計クエリ - 商品統計
TYPESCRIPT
// server/api/admin/stats.get.ts
export default defineEventHandler(async () => {
const [
totalProducts,
totalUsers,
totalOrders,
revenue,
avgPrice
] = await Promise.all([
prisma.product.count(),
prisma.user.count(),
prisma.order.count(),
prisma.order.aggregate({
_sum: { total: true },
where: { status: 'PAID' }
}),
prisma.product.aggregate({
_avg: { price: true }
})
])
return {
totalProducts,
totalUsers,
totalOrders,
totalRevenue: revenue._sum.total || 0,
averagePrice: avgPrice._avg.price || 0
}
})
出力:
TEXT
// 実行成功
6. データベースマイグレーション
(1) ▶サンプル:Prismaマイグレーションコマンド
BASH
# スキーマ変更からマイグレーションを作成
npx prisma migrate dev --name init
# 本番でマイグレーションを適用
npx prisma migrate deploy
# データベースをリセット (開発のみ!)
npx prisma migrate reset
# Prismaクライアントを生成
npx prisma generate
# Prisma Studioを開く (ビジュアルDBブラウザ)
npx prisma studio
# テストデータでシード
npx prisma db seed
出力:
TEXT
# コマンド実行成功
(2) ▶サンプル:シードデータ
TYPESCRIPT
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// カテゴリの作成
const electronics = await prisma.category.create({
data: { name: 'Electronics', slug: 'electronics' }
})
const clothing = await prisma.category.create({
data: { name: 'Clothing', slug: 'clothing' }
})
// 商品の作成
for (let i = 1; i <= 1000; i++) {
await prisma.product.create({
data: {
name: `Product ${i}`,
slug: `product-${i}`,
price: Math.round(Math.random() * 500 * 100) / 100,
categoryId: i % 2 === 0 ? electronics.id : clothing.id,
inStock: Math.random() > 0.2,
rating: Math.round(Math.random() * 5 * 10) / 10,
reviewCount: Math.floor(Math.random() * 5000)
}
})
}
// 管理者ユーザーの作成
await prisma.user.create({
data: {
email: 'bob@megashop.com',
name: 'Bob Admin',
role: 'ADMIN'
}
})
console.log('Seed completed: 1 thousand products + 2 categories + 1 admin')
}
main()
出力:
TEXT
// 実行成功
7. 総合例:MegaShopデータベース統合
TEXT
# .env
DATABASE_URL="postgresql://megashop:password@localhost:5432/megashop"
REDIS_URL="redis://localhost:6379"
JWT_ACCESS_SECRET="your-access-secret"
JWT_REFRESH_SECRET="your-refresh-secret"
TYPESCRIPT
// server/api/products/search.get.ts - 高度な検索
export default defineEventHandler(async (event) => {
const { q, category, minPrice, maxPrice, sort, page, limit } = getQuery(event)
const where = {
AND: [
q ? { OR: [{ name: { contains: q as string, mode: 'insensitive' } }, { description: { contains: q as string, mode: 'insensitive' } }] } : {},
category ? { category: { slug: category as string } } : {},
minPrice ? { price: { gte: Number(minPrice) } } : {},
maxPrice ? { price: { lte: Number(maxPrice) } } : {}
]
}
const orderBy: any = sort === 'price-asc' ? { price: 'asc' }
: sort === 'price-desc' ? { price: 'desc' }
: sort === 'rating' ? { rating: 'desc' }
: { createdAt: 'desc' }
const [items, total] = await Promise.all([
prisma.product.findMany({
where, orderBy,
skip: ((Number(page) || 1) - 1) * (Number(limit) || 20),
take: Number(limit) || 20,
include: { category: { select: { name: true, slug: true } } }
}),
prisma.product.count({ where })
])
return { items, total, page: Number(page) || 1, limit: Number(limit) || 20 }
})
❓よくある質問
Q PrismaとTypeORMの違いは何ですか?
A Prismaは宣言型スキーマを使用し, クライアント側のコードを生成するため, より包括的な型推論を提供します。TypeORMはデコレータを使用し, 従来のORMに近いです。Nuxt 3コミュニティではPrismaが推奨されています。
Q なぜPrismaのシングルトンにグローバル変数が必要なのですか?
A 開発環境では, Nuxtのホットリロードがサーバーコードを再実行します。グローバル変数がないと, 複数のPrismaClientインスタンスが生成され, 接続リークが発生します。本番環境ではこの問題は発生しません。
Q 数百万レコードでのクエリパフォーマンスはどうですか?
A Prismaの
findManyとskip/takeの組み合わせはLIMIT/OFFSETページネーションを生成しますが, 数百万件のデータセットでOFFSET値が大きくなると遅くなります。大規模データセットにはカーソルベースのページネーション (cursor + take)を使用してください。Q PrismaはMongoDBをサポートしていますか?
A はい, ただしプレビュー機能です。PostgreSQLがPrismaとの最適な組み合わせであり, 最も完全な機能セットを提供します。MegaShopではPostgreSQLを推奨しています。
Q トランザクション内で複数の書き込み操作を実行できますか?
A はい。
prisma.$transaction内で任意の数の読み書き操作を実行できます。すべての操作が成功した場合のみトランザクションがコミットされ, 1つでも失敗するとロールバックされます。Q シードスクリプトはどのように実行しますか?
A package.jsonに
"prisma": { "seed": "npx ts-node prisma/seed.ts" }を追加し, npx prisma db seedを実行してください。📖まとめ
- Prismaはスキーマ定義, 自動マイグレーション, 型安全なクエリ, ビジュアルStudioを提供
- server/utils/prisma.tsでシングルトン接続を実装し, 開発環境での接続リークを防止
- CRUD:findManyでページネーション + findUniqueで詳細 + create + update + delete
- $transactionにより, 注文作成, 在庫更新, ショッピングカートクリアが原子的に実行されることを保証
- 数百万件のデータセット:インデックス最適化 + カーソルページネーション + 集計クエリで統計
📝練習問題
- 基本問題 (難易度:⭐):PrismaとPostgreSQLをインストールし, ProductとCategoryモデルを作成して,
migrateとseedを実行してください - 応用問題 (難易度:⭐⭐):完全な商品CRUD APIを実装し, 以前のモックデータを置き換え, ページネーションとカテゴリフィルタリングをサポートしてください
- チャレンジ (難易度:⭐⭐⭐):注文トランザクションを実装してください - 注文の作成, 在庫の引き落とし, ショッピングカートのクリアをすべて1つのトランザクション内で行い, いずれかのステップが失敗した場合はトランザクション全体をロールバックしてください
---|



