Prisma ORM完全ガイド — スキーマ設計・マイグレーション・クエリ・本番運用


現代のNode.js/TypeScript開発において、データベースアクセス層の設計はアプリケーションの品質を左右する重要な要素だ。Prisma ORMは、型安全性・開発体験・本番パフォーマンスを高次元で両立する次世代ORMとして、急速に普及している。

本記事では、Prismaの基本セットアップから本番運用まで、TypeScriptコード例を交えながら体系的に解説する。


1. Prismaとは — TypeORM・Drizzle ORMとの比較

Prismaの基本思想

Prismaは「スキーマファースト」のORMだ。schema.prismaという単一ファイルにデータモデルを定義し、そこから型安全なクライアントコード・マイグレーションファイルを自動生成する。

開発フロー:
schema.prisma → prisma generate → Prisma Client(型安全)
schema.prisma → prisma migrate → SQLマイグレーション

この設計思想により、「スキーマがソースオブトゥルース」となり、TypeScriptの型定義と実際のDB構造が常に一致した状態を保てる。

主要ORM比較

特徴PrismaTypeORMDrizzle ORM
型安全性最高(自動生成)中(デコレータ)高(スキーマ定義)
学習コスト低〜中低〜中
スキーマ定義独自DSLTypeScript ClassTypeScript Object
マイグレーション自動管理自動/手動手動
クエリビルダーFluent APIQueryBuilderSQL-like
バンドルサイズ大(生成コード)
本番実績豊富豊富新興
エコシステム活発活発成長中

Prismaを選ぶべきケース:

  • TypeScriptプロジェクトで型安全性を最優先する
  • チーム開発でスキーマを中心に設計したい
  • マイグレーション管理を自動化したい
  • Prisma Studio(GUIツール)を活用したい

TypeORMが適するケース:

  • Active Recordパターンに慣れている
  • 複雑なクエリを細かく制御したい
  • Javaバックグラウンドからの移行

Drizzle ORMが適するケース:

  • バンドルサイズを最小化したい
  • Edge環境(Cloudflare Workers等)で動作させる
  • SQLに近い表現でクエリを書きたい

2. セットアップ

インストール

# 新規プロジェクト
mkdir my-app && cd my-app
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init

# Prismaインストール
npm install prisma --save-dev
npm install @prisma/client

# Prisma初期化(PostgreSQLの場合)
npx prisma init --datasource-provider postgresql

npx prisma init実行後、以下のファイルが生成される:

prisma/
  schema.prisma    ← スキーマ定義ファイル
.env               ← DATABASE_URL等の環境変数

データベース接続設定

.envファイルに接続文字列を設定する:

# PostgreSQL
DATABASE_URL="postgresql://username:password@localhost:5432/mydb?schema=public"

# MySQL
DATABASE_URL="mysql://username:password@localhost:3306/mydb"

# SQLite(開発用)
DATABASE_URL="file:./dev.db"

# MongoDB
DATABASE_URL="mongodb+srv://username:password@cluster.mongodb.net/mydb"

本番環境では環境変数を適切に管理し、接続文字列をコードにハードコードしないこと。

TypeScript設定

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

3. スキーマ定義

schema.prismaの基本構造

// prisma/schema.prisma

// データソース設定
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// クライアント生成設定
generator client {
  provider = "prisma-client-js"
}

// モデル定義
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

主要アトリビュート

@id — プライマリキー

model User {
  // 自動インクリメント整数ID
  id Int @id @default(autoincrement())

  // UUID(分散システムに最適)
  id String @id @default(uuid())

  // CUID(ソート可能なID)
  id String @id @default(cuid())
}

@unique — ユニーク制約

model User {
  id       Int    @id @default(autoincrement())
  email    String @unique
  username String @unique

  // 複合ユニーク制約
  @@unique([firstName, lastName])
}

@default — デフォルト値

model Post {
  id          Int      @id @default(autoincrement())
  title       String
  published   Boolean  @default(false)
  viewCount   Int      @default(0)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  slug        String   @default("")
}

@index — インデックス

model Post {
  id        Int    @id @default(autoincrement())
  title     String
  authorId  Int
  status    String

  // 単一インデックス
  @@index([authorId])

  // 複合インデックス
  @@index([status, createdAt])
}

@map — テーブル名・カラム名マッピング

model User {
  id        Int    @id @default(autoincrement())
  firstName String @map("first_name")  // DBカラム名: first_name
  lastName  String @map("last_name")

  @@map("users")  // DBテーブル名: users
}

4. データ型

スカラー型一覧

model DataTypes {
  id          Int      @id @default(autoincrement())

  // 文字列
  name        String           // VARCHAR / TEXT
  description String?          // NULL許可
  bio         String   @db.Text  // DBネイティブTEXT型

  // 数値
  age         Int              // INTEGER
  score       Float            // FLOAT / DOUBLE
  price       Decimal          // DECIMAL(金融計算に必須)
  bigNumber   BigInt           // BIGINT

  // 論理値
  isActive    Boolean          // BOOLEAN

  // 日時
  createdAt   DateTime         // TIMESTAMP
  birthDate   DateTime @db.Date  // DATE型のみ

  // JSON(PostgreSQL/MySQL対応)
  metadata    Json             // JSONB / JSON

  // バイナリ
  avatar      Bytes            // BYTEA / BLOB

  // 列挙型
  role        Role             // ENUM
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

Decimal型の重要性

金融・決済システムではFloatではなくDecimalを使うべきだ:

import { Prisma } from '@prisma/client'

// 正しい: Decimal型で精度を保証
const product = await prisma.product.create({
  data: {
    name: 'Premium Plan',
    price: new Prisma.Decimal('9.99'),  // 文字列から初期化
    tax: new Prisma.Decimal('0.10'),
  }
})

// 誤り: Float型は浮動小数点誤差が発生する
// price: 9.99  → 実際: 9.990000000000001

Json型の活用

// スキーマ定義
model Product {
  id          Int   @id @default(autoincrement())
  name        String
  metadata    Json  // 動的な属性を格納
  settings    Json  @default("{}")
}

// 型定義(Prisma側は自動生成なし)
interface ProductMetadata {
  color?: string
  size?: string[]
  specifications?: Record<string, string>
}

// 作成
const product = await prisma.product.create({
  data: {
    name: 'T-Shirt',
    metadata: {
      color: 'blue',
      size: ['S', 'M', 'L', 'XL'],
      specifications: {
        material: 'cotton',
        weight: '200g'
      }
    } satisfies ProductMetadata,
  }
})

// JSON内のフィールドでフィルタリング
const blueProducts = await prisma.product.findMany({
  where: {
    metadata: {
      path: ['color'],
      equals: 'blue'
    }
  }
})

5. リレーション

1対1リレーション

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  profile Profile?  // オプショナル1対1
}

model Profile {
  id        Int    @id @default(autoincrement())
  bio       String?
  avatarUrl String?
  userId    Int    @unique  // 外部キー(@uniqueで1対1を保証)
  user      User   @relation(fields: [userId], references: [id])
}
// ユーザーとプロフィールを同時作成
const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    profile: {
      create: {
        bio: 'Full-stack developer',
        avatarUrl: 'https://example.com/avatar.jpg'
      }
    }
  },
  include: {
    profile: true
  }
})

1対多リレーション

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  posts Post[] // ユーザーは複数の投稿を持つ
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  comments  Comment[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  postId    Int
  post      Post     @relation(fields: [postId], references: [id])
}
// 1対多: ユーザーの全投稿を取得
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      take: 10,
    }
  }
})

// 1対多: 投稿とそのコメントを取得
const postWithComments = await prisma.post.findUnique({
  where: { id: 1 },
  include: {
    author: true,
    comments: {
      include: {
        author: true
      }
    }
  }
})

多対多リレーション

// 暗黙的多対多(Prismaが中間テーブルを自動管理)
model Post {
  id   Int    @id @default(autoincrement())
  tags Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

// 明示的多対多(中間テーブルに追加フィールドが必要な場合)
model Post {
  id         Int               @id @default(autoincrement())
  categories CategoriesOnPosts[]
}

model Category {
  id    Int               @id @default(autoincrement())
  name  String
  posts CategoriesOnPosts[]
}

model CategoriesOnPosts {
  postId     Int
  categoryId Int
  assignedAt DateTime @default(now())
  assignedBy String

  post     Post     @relation(fields: [postId], references: [id])
  category Category @relation(fields: [categoryId], references: [id])

  @@id([postId, categoryId])  // 複合プライマリキー
}
// 暗黙的多対多: タグを接続
const post = await prisma.post.update({
  where: { id: 1 },
  data: {
    tags: {
      connect: [
        { id: 1 },
        { id: 2 },
      ],
      // または新規作成
      create: [
        { name: 'TypeScript' },
        { name: 'Prisma' },
      ]
    }
  },
  include: { tags: true }
})

// 明示的多対多: 中間テーブルに追加情報を含めて作成
const assignment = await prisma.categoriesOnPosts.create({
  data: {
    postId: 1,
    categoryId: 2,
    assignedAt: new Date(),
    assignedBy: 'admin@example.com'
  }
})

自己参照リレーション

model Category {
  id       Int        @id @default(autoincrement())
  name     String
  parentId Int?
  parent   Category?  @relation("CategoryTree", fields: [parentId], references: [id])
  children Category[] @relation("CategoryTree")
}

model Employee {
  id         Int        @id @default(autoincrement())
  name       String
  managerId  Int?
  manager    Employee?  @relation("Management", fields: [managerId], references: [id])
  reports    Employee[] @relation("Management")
}

6. マイグレーション

prisma migrate dev(開発環境)

# マイグレーションを作成して適用
npx prisma migrate dev --name init

# マイグレーション名を指定(スキーマ変更後)
npx prisma migrate dev --name add_user_profile

# ドライラン(SQLを確認するだけ)
npx prisma migrate dev --create-only

生成されるディレクトリ構造:

prisma/
  migrations/
    20260101000000_init/
      migration.sql
    20260115000000_add_user_profile/
      migration.sql
  schema.prisma

生成されるmigration.sqlの例:

-- CreateTable
CREATE TABLE "users" (
    "id" SERIAL NOT NULL,
    "email" TEXT NOT NULL,
    "name" TEXT,
    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");

prisma migrate deploy(本番環境)

# 本番環境へのデプロイ(未適用のマイグレーションを全て実行)
npx prisma migrate deploy

migrate deploymigrate devと異なり:

  • インタラクティブなプロンプトを表示しない
  • シャドウデータベースを使用しない
  • CI/CDパイプラインに適している

prisma migrate reset

# データベースをリセット(全データ削除 + マイグレーション再実行)
npx prisma migrate reset

# シードも実行
npx prisma migrate reset --skip-seed

警告: migrate resetは全データを削除する。本番環境では絶対に実行しないこと。

マイグレーション状態の確認

# マイグレーション状態を確認
npx prisma migrate status

出力例:

3 migrations found in prisma/migrations

Database schema is up to date!

The following migration(s) were applied:

  2026-01-01_init (applied)
  2026-01-15_add_profile (applied)
  2026-02-01_add_posts (applied)

カスタムマイグレーションSQL

自動生成のSQLに加えてカスタムSQLを実行する場合:

-- prisma/migrations/20260201_add_posts/migration.sql
-- Prismaが生成したSQL(自動)
CREATE TABLE "posts" (
    "id" SERIAL NOT NULL,
    "title" TEXT NOT NULL,
    ...
);

-- カスタム追加SQL(手動で追記)
-- フルテキスト検索インデックスを追加
CREATE INDEX "posts_title_search_idx" ON "posts" USING GIN (to_tsvector('japanese', "title"));

-- 既存データの変換
UPDATE "posts" SET "slug" = lower(regexp_replace("title", '\s+', '-', 'g'));

7. Prisma Client — CRUD操作

クライアントの初期化

// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'

// シングルトンパターン(開発環境でのホットリロード対策)
declare global {
  var prisma: PrismaClient | undefined
}

export const prisma =
  global.prisma ||
  new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'error', 'warn']
      : ['error'],
  })

if (process.env.NODE_ENV !== 'production') {
  global.prisma = prisma
}

Create(作成)

import { prisma } from './lib/prisma'

// 単一レコード作成
const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
  }
})
console.log(user.id, user.email)  // 型安全: User型が推論される

// ネストした作成(リレーション含む)
const userWithProfile = await prisma.user.create({
  data: {
    email: 'bob@example.com',
    name: 'Bob',
    profile: {
      create: {
        bio: 'Backend Engineer',
      }
    },
    posts: {
      create: [
        { title: 'First Post', published: true },
        { title: 'Draft Post', published: false },
      ]
    }
  },
  include: {
    profile: true,
    posts: true,
  }
})

// バルク作成
const users = await prisma.user.createMany({
  data: [
    { email: 'carol@example.com', name: 'Carol' },
    { email: 'dave@example.com', name: 'Dave' },
    { email: 'eve@example.com', name: 'Eve' },
  ],
  skipDuplicates: true,  // 重複をスキップ
})
console.log(`${users.count}件作成`)

Read(読み取り)

// IDで単一レコード取得(存在しない場合はnull)
const user = await prisma.user.findUnique({
  where: { id: 1 }
})

// 存在しない場合は例外をスロー
const userOrThrow = await prisma.user.findUniqueOrThrow({
  where: { id: 999 }
})

// 全件取得
const allUsers = await prisma.user.findMany()

// 条件付き取得
const activeUsers = await prisma.user.findMany({
  where: {
    isActive: true,
    createdAt: {
      gte: new Date('2026-01-01')
    }
  },
  orderBy: { createdAt: 'desc' },
  take: 20,
  skip: 0,
})

// 最初の1件
const firstUser = await prisma.user.findFirst({
  where: { role: 'ADMIN' },
  orderBy: { createdAt: 'asc' }
})

Update(更新)

// 単一レコード更新
const updatedUser = await prisma.user.update({
  where: { id: 1 },
  data: {
    name: 'Alice Updated',
    updatedAt: new Date(),
  }
})

// インクリメント/デクリメント
const post = await prisma.post.update({
  where: { id: 1 },
  data: {
    viewCount: { increment: 1 },  // +1
    likeCount: { decrement: 1 },  // -1
    score: { multiply: 1.1 },     // x1.1
  }
})

// Upsert(存在すれば更新、なければ作成)
const upserted = await prisma.user.upsert({
  where: { email: 'alice@example.com' },
  update: { name: 'Alice v2' },
  create: {
    email: 'alice@example.com',
    name: 'Alice v2',
  }
})

// バルク更新
const result = await prisma.post.updateMany({
  where: { authorId: 1, published: false },
  data: { published: true }
})
console.log(`${result.count}件更新`)

Delete(削除)

// 単一レコード削除
const deleted = await prisma.user.delete({
  where: { id: 1 }
})

// バルク削除
const deletedPosts = await prisma.post.deleteMany({
  where: {
    createdAt: {
      lt: new Date('2025-01-01')
    }
  }
})
console.log(`${deletedPosts.count}件削除`)

8. 高度なクエリ

where句の詳細

// 比較演算子
const users = await prisma.user.findMany({
  where: {
    age: {
      gte: 18,    // >=
      lte: 65,    // <=
      gt: 20,     // >
      lt: 60,     // <
      not: 30,    // !=
    }
  }
})

// 文字列フィルタ
const posts = await prisma.post.findMany({
  where: {
    title: {
      contains: 'Prisma',       // LIKE '%Prisma%'
      startsWith: 'How to',     // LIKE 'How to%'
      endsWith: 'Guide',        // LIKE '%Guide'
      mode: 'insensitive',      // 大文字小文字無視
    }
  }
})

// IN句
const specificUsers = await prisma.user.findMany({
  where: {
    id: { in: [1, 2, 3, 4, 5] }
  }
})

// 論理演算子(AND/OR/NOT)
const filteredPosts = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      { createdAt: { gte: new Date('2026-01-01') } },
    ],
    OR: [
      { title: { contains: 'TypeScript' } },
      { title: { contains: 'JavaScript' } },
    ],
    NOT: {
      authorId: 999
    }
  }
})

// リレーション越しのフィルタ
const usersWithPosts = await prisma.user.findMany({
  where: {
    posts: {
      some: {  // 少なくとも1つのポストが条件を満たす
        published: true,
      }
    }
  }
})

const usersAllPublished = await prisma.user.findMany({
  where: {
    posts: {
      every: {  // 全てのポストが条件を満たす
        published: true,
      }
    }
  }
})

const usersNoPost = await prisma.user.findMany({
  where: {
    posts: {
      none: {}  // ポストがない
    }
  }
})

select — 返すフィールドを指定

// 特定フィールドのみ取得
const userNames = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    name: true,
    // password: false (デフォルト)
  }
})
// 型: { id: number; email: string; name: string | null }[]

// ネストしたselectで必要なフィールドのみ取得
const postsWithAuthorName = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    author: {
      select: {
        id: true,
        name: true,
        // emailを返さない(セキュリティ)
      }
    },
    _count: {
      select: { comments: true }  // コメント数のみ
    }
  }
})

include — リレーションを含める

// 関連モデルを含めて取得
const post = await prisma.post.findUnique({
  where: { id: 1 },
  include: {
    author: true,            // User全フィールド
    comments: {
      include: {
        author: {            // コメントの著者も含める
          select: { name: true }
        }
      },
      where: { approved: true },
      orderBy: { createdAt: 'asc' },
      take: 5,
    },
    tags: true,
    _count: {
      select: { comments: true, likes: true }
    }
  }
})

orderBy — ソート

// 単一フィールドソート
const posts = await prisma.post.findMany({
  orderBy: { createdAt: 'desc' }
})

// 複数フィールドソート
const users = await prisma.user.findMany({
  orderBy: [
    { role: 'asc' },
    { name: 'asc' },
    { createdAt: 'desc' },
  ]
})

// リレーション越しのソート
const postsByAuthorName = await prisma.post.findMany({
  orderBy: {
    author: { name: 'asc' }
  }
})

// _count(件数)でソート
const popularPosts = await prisma.post.findMany({
  orderBy: {
    comments: { _count: 'desc' }
  }
})

ページネーション

// オフセットページネーション
async function getPostsPaginated(page: number, perPage: number = 20) {
  const [posts, total] = await prisma.$transaction([
    prisma.post.findMany({
      skip: (page - 1) * perPage,
      take: perPage,
      orderBy: { createdAt: 'desc' },
    }),
    prisma.post.count()
  ])

  return {
    data: posts,
    pagination: {
      total,
      page,
      perPage,
      totalPages: Math.ceil(total / perPage),
    }
  }
}

// カーソルベースページネーション(大規模データに最適)
async function getPostsCursor(cursor?: number, take: number = 20) {
  const posts = await prisma.post.findMany({
    take,
    skip: cursor ? 1 : 0,  // カーソルの次から取得
    cursor: cursor ? { id: cursor } : undefined,
    orderBy: { id: 'asc' },
  })

  const nextCursor = posts.length === take
    ? posts[posts.length - 1].id
    : undefined

  return { posts, nextCursor }
}

9. トランザクション

$transaction — 基本的な使い方

// 複数操作をアトミックに実行
const [debitAccount, creditAccount] = await prisma.$transaction([
  prisma.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 1000 } }
  }),
  prisma.account.update({
    where: { id: 2 },
    data: { balance: { increment: 1000 } }
  }),
])
// 両方成功 or 両方ロールバック

インタラクティブトランザクション

// 複雑な条件分岐が必要な場合
const result = await prisma.$transaction(async (tx) => {
  // 残高確認
  const sourceAccount = await tx.account.findUniqueOrThrow({
    where: { id: 1 }
  })

  if (sourceAccount.balance < 1000) {
    throw new Error('残高不足')  // これでロールバック
  }

  // 転送実行
  const debit = await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 1000 } }
  })

  const credit = await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 1000 } }
  })

  // 取引履歴を記録
  const transaction = await tx.transactionLog.create({
    data: {
      fromAccountId: 1,
      toAccountId: 2,
      amount: 1000,
      status: 'COMPLETED',
    }
  })

  return { debit, credit, transaction }
})

トランザクションオプション

// タイムアウトと分離レベルの設定
const result = await prisma.$transaction(
  async (tx) => {
    // トランザクション内の操作
    const order = await tx.order.create({
      data: { userId: 1, total: 5000 }
    })

    await tx.inventory.update({
      where: { productId: order.productId },
      data: { quantity: { decrement: 1 } }
    })

    return order
  },
  {
    maxWait: 5000,      // 接続待機最大時間(ms)
    timeout: 10000,     // トランザクション最大実行時間(ms)
    isolationLevel: 'Serializable',  // 分離レベル
  }
)

Nested writes(ネストした書き込み)

// ネストした作成・接続・切断
const post = await prisma.post.create({
  data: {
    title: '新しい記事',
    author: {
      connect: { id: 1 }  // 既存ユーザーに接続
    },
    tags: {
      connectOrCreate: [  // 存在すれば接続、なければ作成
        {
          where: { name: 'TypeScript' },
          create: { name: 'TypeScript' }
        },
        {
          where: { name: 'Prisma' },
          create: { name: 'Prisma' }
        }
      ]
    },
    categories: {
      create: [
        { name: '技術' }  // 新規カテゴリ作成
      ]
    }
  }
})

// ネストした更新
const updatedPost = await prisma.post.update({
  where: { id: 1 },
  data: {
    title: '更新された記事',
    tags: {
      disconnect: [{ id: 3 }],  // タグを切断
      connect: [{ id: 5 }],     // タグを接続
    }
  }
})

10. 集計クエリ

count・sum・avg・min・max

// レコード数カウント
const totalPosts = await prisma.post.count()
const publishedCount = await prisma.post.count({
  where: { published: true }
})

// 集計関数
const stats = await prisma.order.aggregate({
  where: {
    status: 'COMPLETED',
    createdAt: {
      gte: new Date('2026-01-01'),
      lt: new Date('2026-02-01'),
    }
  },
  _count: {
    _all: true,       // 全件数
    userId: true,     // userId非NULLの件数
  },
  _sum: {
    amount: true,     // 合計金額
  },
  _avg: {
    amount: true,     // 平均金額
  },
  _min: {
    amount: true,     // 最小金額
    createdAt: true,  // 最古の日時
  },
  _max: {
    amount: true,     // 最大金額
    createdAt: true,  // 最新の日時
  }
})

console.log('月次売上統計:')
console.log('件数:', stats._count._all)
console.log('合計:', stats._sum.amount)
console.log('平均:', stats._avg.amount)

groupBy — グループ集計

// カテゴリ別投稿数
const postsByCategory = await prisma.post.groupBy({
  by: ['categoryId', 'published'],
  where: {
    createdAt: { gte: new Date('2026-01-01') }
  },
  _count: {
    _all: true
  },
  _sum: {
    viewCount: true
  },
  having: {
    _count: {
      _all: { gt: 5 }  // 5件以上のグループのみ
    }
  },
  orderBy: {
    _count: {
      _all: 'desc'
    }
  }
})

// 月別売上集計
const monthlySales = await prisma.order.groupBy({
  by: ['month'],  // 計算フィールドはRaw Queryが必要
  _sum: { amount: true },
  _count: { _all: true },
})

11. Raw Queries

$queryRaw — SELECT文

import { Prisma } from '@prisma/client'

// テンプレートリテラル(SQLインジェクション対策済み)
const userId = 1
const users = await prisma.$queryRaw<User[]>`
  SELECT * FROM users
  WHERE id = ${userId}
  AND is_active = true
`

// 複雑な集計(ORMでは表現しにくいクエリ)
const monthlySales = await prisma.$queryRaw<
  Array<{ month: string; total: number; count: number }>
>`
  SELECT
    TO_CHAR(created_at, 'YYYY-MM') AS month,
    SUM(amount) AS total,
    COUNT(*) AS count
  FROM orders
  WHERE status = 'COMPLETED'
    AND created_at >= NOW() - INTERVAL '12 months'
  GROUP BY TO_CHAR(created_at, 'YYYY-MM')
  ORDER BY month DESC
`

// Prisma.sqlを使った動的クエリ構築
async function searchUsers(name?: string, role?: string) {
  let query = Prisma.sql`SELECT * FROM users WHERE 1=1`

  if (name) {
    query = Prisma.sql`${query} AND name ILIKE ${'%' + name + '%'}`
  }
  if (role) {
    query = Prisma.sql`${query} AND role = ${role}`
  }

  return prisma.$queryRaw(query)
}

$executeRaw — INSERT/UPDATE/DELETE文

// バルク更新(複雑なロジック)
const affectedRows = await prisma.$executeRaw`
  UPDATE posts
  SET
    slug = lower(regexp_replace(title, '\s+', '-', 'g')),
    updated_at = NOW()
  WHERE slug IS NULL OR slug = ''
`
console.log(`${affectedRows}件のslugを更新`)

// カスタムインデックス操作
await prisma.$executeRaw`
  CREATE INDEX CONCURRENTLY IF NOT EXISTS
  posts_fulltext_idx ON posts
  USING GIN (to_tsvector('japanese', title || ' ' || coalesce(content, '')))
`

注意: $queryRaw$executeRawはテンプレートリテラルを使うこと。文字列連結によるSQLはPrisma.raw()でラップしなければSQLインジェクションの危険がある。


12. 接続プール設定

接続URLパラメータ

# PostgreSQL接続プール設定
DATABASE_URL="postgresql://user:pass@host:5432/db?schema=public&connection_limit=10&pool_timeout=20&connect_timeout=10"
パラメータデフォルト説明
connection_limitCPU数×2+1接続プールの最大接続数
pool_timeout10秒接続待機タイムアウト
connect_timeout5秒DB接続タイムアウト
socket_timeout設定なしソケットタイムアウト

PrismaClientの設定

// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
  return new PrismaClient({
    // ログ設定
    log: [
      { emit: 'event', level: 'query' },
      { emit: 'event', level: 'error' },
      { emit: 'stdout', level: 'warn' },
    ],
    // エラーフォーマット
    errorFormat: 'pretty',
    // データソース設定(URLを上書き可能)
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
  })
}

const prisma = prismaClientSingleton()

// クエリログをメトリクス収集に活用
prisma.$on('query', (event) => {
  if (event.duration > 1000) {
    console.warn(`Slow query (${event.duration}ms):`, event.query)
  }
})

prisma.$on('error', (event) => {
  console.error('Prisma error:', event.message)
})

export { prisma }

サーバーレス環境での接続プール

Next.js等のサーバーレス環境では接続数の管理が重要だ:

// Vercel/Netlify等サーバーレス環境
// Prisma Accelerate(接続プーキング as a Service)を使う
// DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=..."

// または Supabase Connection Pooler (PgBouncer)
// DATABASE_URL="postgresql://...@db.xxx.supabase.co:6543/postgres?pgbouncer=true"

// サーバーレスでの推奨設定
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL + '&connection_limit=1',
    }
  }
})

13. 本番運用

prisma migrate deploy — CI/CDへの組み込み

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Generate Prisma Client
        run: npx prisma generate

      - name: Run database migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

      - name: Deploy application
        run: npm run build && npm start

シード(初期データ投入)

// prisma/seed.ts
import { PrismaClient, Role } from '@prisma/client'
import bcrypt from 'bcrypt'

const prisma = new PrismaClient()

async function main() {
  console.log('シード開始...')

  // 管理者ユーザー作成
  const admin = await prisma.user.upsert({
    where: { email: 'admin@example.com' },
    update: {},
    create: {
      email: 'admin@example.com',
      name: 'System Admin',
      role: Role.ADMIN,
      password: await bcrypt.hash('admin123', 12),
    }
  })

  // カテゴリ作成
  const categories = await Promise.all([
    prisma.category.upsert({
      where: { slug: 'technology' },
      update: {},
      create: { name: 'テクノロジー', slug: 'technology' }
    }),
    prisma.category.upsert({
      where: { slug: 'business' },
      update: {},
      create: { name: 'ビジネス', slug: 'business' }
    }),
  ])

  // サンプル投稿
  await prisma.post.createMany({
    data: [
      {
        title: 'Prisma ORMの基本',
        slug: 'prisma-orm-basics',
        published: true,
        authorId: admin.id,
        categoryId: categories[0].id,
      },
      {
        title: 'TypeScriptベストプラクティス',
        slug: 'typescript-best-practices',
        published: true,
        authorId: admin.id,
        categoryId: categories[0].id,
      },
    ],
    skipDuplicates: true,
  })

  console.log('シード完了')
  console.log('作成:', { admin, categories: categories.length })
}

main()
  .catch((e) => {
    console.error(e)
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

package.jsonにシードスクリプトを登録:

{
  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
  },
  "scripts": {
    "db:seed": "npx prisma db seed",
    "db:reset": "npx prisma migrate reset",
    "db:studio": "npx prisma studio"
  }
}

ステージング環境の管理

# ステージング環境専用の.envファイル
# .env.staging
DATABASE_URL="postgresql://user:pass@staging-host:5432/myapp_staging"
// prisma/schema.prisma(マルチDB対応)
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // shadowDatabaseUrl: マイグレーション生成時に使用
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
# ステージングへのマイグレーション適用
DATABASE_URL="..." npx prisma migrate deploy

# ステージングでシード実行
DATABASE_URL="..." npx prisma db seed

Prisma Studio(開発用GUI)

# ブラウザでデータ確認・編集
npx prisma studio
# → http://localhost:5555 が開く

Prisma Studioは開発・デバッグ時に非常に便利だが、本番環境データへのアクセスには使わないこと。

ヘルスチェックとモニタリング

// src/lib/health.ts
import { prisma } from './prisma'

export async function checkDatabaseHealth(): Promise<boolean> {
  try {
    await prisma.$queryRaw`SELECT 1`
    return true
  } catch (error) {
    console.error('Database health check failed:', error)
    return false
  }
}

// APIルートでヘルスチェック
// app/api/health/route.ts
import { checkDatabaseHealth } from '@/lib/health'

export async function GET() {
  const dbHealthy = await checkDatabaseHealth()

  return Response.json({
    status: dbHealthy ? 'ok' : 'error',
    database: dbHealthy ? 'connected' : 'disconnected',
    timestamp: new Date().toISOString(),
  }, {
    status: dbHealthy ? 200 : 503
  })
}

グレースフルシャットダウン

// src/index.ts
import { prisma } from './lib/prisma'

process.on('SIGINT', async () => {
  console.log('シャットダウン中...')
  await prisma.$disconnect()
  process.exit(0)
})

process.on('SIGTERM', async () => {
  await prisma.$disconnect()
  process.exit(0)
})

まとめ

Prismaは現代のTypeScript開発における最も洗練されたORMの一つだ。主な強みを整理すると:

型安全性の徹底: スキーマから自動生成されるPrisma Clientは、クエリ結果の型を正確に推論する。includeselectの組み合わせによって返る型が変わり、TypeScriptコンパイラがランタイムエラーを事前に検出できる。

開発体験の優位性: prisma migrate devによる自動マイグレーション生成、Prisma StudioによるGUI、詳細なエラーメッセージが開発サイクルを高速化する。

本番環境の信頼性: prisma migrate deployを使ったCI/CDへの統合、接続プール管理、インタラクティブトランザクションにより、本番環境での安定稼働を実現できる。

注意すべき点:

  • N+1問題: includeを適切に使い、ループ内でクエリを実行しないこと
  • バンドルサイズ: Prisma Clientは生成コードが大きいため、Edge環境ではPrisma Accelerateの利用を検討する
  • マイグレーション管理: prisma/migrationsディレクトリはgit管理下に置き、チーム全員が同じ状態を保つ

DevToolBoxでJSON/スキーマ検証を効率化

PrismaのJson型フィールドを扱う際、JSONデータのバリデーションや構造確認が頻繁に発生する。開発中にJSONを素早く検証・フォーマットしたいなら、DevToolBox が便利だ。

DevToolBoxはブラウザ上で動作する開発者向けツールセットで、JSONフォーマッター・バリデーター・差分チェックなど、日常的な開発タスクをワンストップで処理できる。Prismaのmetadataフィールドやsettingsフィールドに格納するJSONデータを事前に検証しておくことで、本番環境でのデータ整合性エラーを防げる。

インストール不要でブラウザから即使えるため、開発チームでの共有にも最適だ。


参考リンク


関連記事