Prisma Client

schema.prisma から自動生成された型付き ORM Client。 find / create / update / delete から where 演算子、ページング、集計まで。

基本

import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

// 各 model に対して prisma.<model 名(小文字)>.メソッド
prisma.user.findMany()
prisma.post.create({ data: { title: "..." } })

SELECT(読み取り)

findMany

const users = await prisma.user.findMany()

// 条件
const adults = await prisma.user.findMany({
  where: { age: { gte: 18 } }
})

// 列を絞る
const list = await prisma.user.findMany({
  select: { id: true, email: true }
})

// ソート + ページング
const page = await prisma.post.findMany({
  where: { published: true },
  orderBy: { createdAt: "desc" },
  skip: 20,
  take: 10,
})

findUnique(主キー / unique 列)

const user = await prisma.user.findUnique({
  where: { id: "abc" }
})
// or
const user2 = await prisma.user.findUnique({
  where: { email: "alice@example.com" }
})

findFirst(最初の 1 件)

const post = await prisma.post.findFirst({
  where: { authorId: userId },
  orderBy: { createdAt: "desc" },
})

findUniqueOrThrow / findFirstOrThrow

// 見つからなければ例外
const user = await prisma.user.findUniqueOrThrow({
  where: { id }
})

where 演算子

prisma.post.findMany({
  where: {
    // 等価
    status: "published",

    // 比較
    views: { gt: 100, lte: 1000 },

    // IN
    category: { in: ["tech", "design"] },
    category2: { notIn: ["draft"] },

    // 文字列
    title: { contains: "react", mode: "insensitive" },
    title: { startsWith: "How" },
    title: { endsWith: "?" },

    // null
    deletedAt: null,
    deletedAt: { not: null },

    // 配列
    tags: { has: "javascript" },
    tags: { hasSome: ["js", "ts"] },
    tags: { hasEvery: ["js", "react"] },

    // AND / OR / NOT
    AND: [{ published: true }, { views: { gt: 100 } }],
    OR: [{ authorId: me }, { collaborators: { some: { userId: me } } }],
    NOT: [{ status: "draft" }],
  }
})

INSERT(作成)

// 1 件
const user = await prisma.user.create({
  data: { email: "alice@example.com", name: "Alice" }
})

// 関連も同時作成(ネスト)
const userWithPosts = await prisma.user.create({
  data: {
    email: "bob@example.com",
    posts: {
      create: [
        { title: "first post" },
        { title: "second post" },
      ]
    }
  },
  include: { posts: true }
})

// 複数件
await prisma.post.createMany({
  data: [
    { title: "a", authorId },
    { title: "b", authorId },
  ],
  skipDuplicates: true,   // 衝突は無視
})

// createManyAndReturn(v5+)
const posts = await prisma.post.createManyAndReturn({
  data: [...]
})

UPDATE

// 1 件(unique で)
await prisma.user.update({
  where: { id },
  data: { name: "Alice 2" }
})

// 複数件
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true }
})

// 数値演算
await prisma.post.update({
  where: { id },
  data: { views: { increment: 1 } }
  // decrement / multiply / divide / set
})

UPSERT

await prisma.user.upsert({
  where: { email },
  update: { lastLogin: new Date() },
  create: { email, name: "(unknown)" }
})

DELETE

await prisma.post.delete({ where: { id } })
await prisma.post.deleteMany({ where: { authorId } })

select と include

select: 必要な列だけ取得

const post = await prisma.post.findUnique({
  where: { id },
  select: {
    id: true,
    title: true,
    author: { select: { id: true, name: true } }
  }
})
// 戻り値の型: { id, title, author: { id, name } }

include: 関連テーブルも一緒に

const post = await prisma.post.findUnique({
  where: { id },
  include: { author: true, comments: true }
})

select と include は同時に使えない(select の中で関連は select する)。

ページング

skip / take(オフセット式)

await prisma.post.findMany({
  skip: page * pageSize,
  take: pageSize,
  orderBy: { createdAt: "desc" },
})

cursor(カーソル式 / 大量データ向け)

await prisma.post.findMany({
  take: 10,
  skip: 1,                       // cursor 自体を含めない
  cursor: { id: lastId },
  orderBy: { id: "asc" },
})

オフセット式は遅くなる(後ろのページほど)。大量データではカーソル式推奨。

count

const total = await prisma.post.count({ where: { published: true } })

// 関連数
const userWithCount = await prisma.user.findUnique({
  where: { id },
  include: { _count: { select: { posts: true } } }
})
console.log(userWithCount?._count.posts)

aggregate

const stats = await prisma.post.aggregate({
  where: { published: true },
  _count: { _all: true },
  _avg: { views: true },
  _max: { views: true },
  _sum: { views: true },
})

groupBy

const byCategory = await prisma.post.groupBy({
  by: ["category"],
  where: { published: true },
  _count: { _all: true },
  _avg: { views: true },
  having: { _count: { _all: { gt: 5 } } },
  orderBy: { _count: { _all: "desc" } },
})

distinct

const cats = await prisma.post.findMany({
  distinct: ["category"],
  select: { category: true },
})

型推論

戻り値の型を取り出す

import { Prisma } from "@prisma/client"

type PostWithAuthor = Prisma.PostGetPayload<{
  include: { author: true }
}>

function render(post: PostWithAuthor) {
  console.log(post.author.name)
}

引数の型

const args = {
  where: { published: true },
  include: { author: true },
} satisfies Prisma.PostFindManyArgs

JSON 列の操作

await prisma.post.update({
  where: { id },
  data: {
    meta: {
      tags: ["js", "react"],
      views: 100,
    }
  }
})

// JSON フィルタ
await prisma.post.findMany({
  where: {
    meta: {
      path: ["tags"],
      array_contains: ["js"]
    }
  }
})

NULL 処理

// 「カテゴリが指定されていれば絞る」のパターン
prisma.post.findMany({
  where: {
    category: filter.category ?? undefined,  // undefined なら無視
  }
})

$queryRaw(生 SQL)

// テンプレートリテラル形式(自動エスケープ)
const result = await prisma.$queryRaw`
  SELECT id, title FROM "Post"
  WHERE created_at > ${oneWeekAgo}
`

// 型を指定
type Row = { id: string, title: string }
const rows = await prisma.$queryRaw<Row[]>`SELECT ...`

$executeRaw(INSERT/UPDATE/DELETE)

await prisma.$executeRaw`UPDATE "Post" SET views = views + 1 WHERE id = ${id}`

$disconnect

await prisma.$disconnect()

スクリプト終了時に呼ぶ。長期プロセス(サーバ)は基本不要。

エラーハンドリング

import { Prisma } from "@prisma/client"

try {
  await prisma.user.create({ data })
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === "P2002") {
      // unique 制約違反
    }
  }
  throw e
}

主要エラーコード

Prisma Client のクエリログ

const prisma = new PrismaClient({ log: ["query", "warn", "error"] })

Optional vs Nullable

連鎖ナビゲーション(Fluent API)

const posts = await prisma.user.findUnique({ where: { id } }).posts()

親の主キーから関連を辿る糖衣構文。チェーンの途中の null は undefined

パフォーマンス Tips

失敗パターン

症状対処
P2002 重複upsert に変える / アプリで先に exist 確認
P2025 not foundfindUnique と組み合わせるか、try/catch
遅いEXPLAIN で確認、index 追加、select 絞る
BigInt の JSON 化失敗JSON.stringify の replacer で string 化
Decimal の比較Decimal.js のメソッドを使う
最初に覚える 5 つ

findMany / findUnique / create / update / delete。 これと where 演算子だけで 80% は片付く。残りは include / select