404 Not Found

404 Not Found


nginx

Prismaデータベース

MegaShopは常にモックデータを使用していました - 商品データはJavaScript配列に格納され, 再起動すると失われます。Charlieは本物のデータベースを必要としています。Bobが書いた生のSQLはエラーが発生しやすく, 型安全性に欠けています。Prisma ORMは型安全なデータベースクエリを提供し, 1行のコードでTypeScript型を生成します。

1. 学ぶ内容


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図

100%
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のfindManyskip/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を実行してください。

📖まとめ


📝練習問題

  1. 基本問題 (難易度:⭐):PrismaとPostgreSQLをインストールし, ProductとCategoryモデルを作成して, migrateseedを実行してください
  2. 応用問題 (難易度:⭐⭐):完全な商品CRUD APIを実装し, 以前のモックデータを置き換え, ページネーションとカテゴリフィルタリングをサポートしてください
  3. チャレンジ (難易度:⭐⭐⭐):注文トランザクションを実装してください - 注文の作成, 在庫の引き落とし, ショッピングカートのクリアをすべて1つのトランザクション内で行い, いずれかのステップが失敗した場合はトランザクション全体をロールバックしてください

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%