Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active March 15, 2025 12:36
Show Gist options
  • Select an option

  • Save yano3nora/63c96a24029db90bf723b1b25fe2f882 to your computer and use it in GitHub Desktop.

Select an option

Save yano3nora/63c96a24029db90bf723b1b25fe2f882 to your computer and use it in GitHub Desktop.
[js: Prisma] Next-generation Node.js and TypeScript ORM. #js

Overview

prisma.io
Prisma Client API reference - prisma.io
prisma/prisma - github.com

  • TypeScript ベース x GraphQL ライクな schema 駆動の ORM ツール
    • schema 定義から TypeScript 向けの型定義を自動生成してくれる
  • GraphQL サーバ内の ORM に使ったり、Next.js などの JS フレームワークから DB クライアントとして使ったり
  • Seeding, Migration なども有しており JAMStack 開発の RDB まわりは一通りこいつに任せていい

Issues

Getting Started

Next.js から Prisma ORM を利用する Next.js + Prisma + NextAuth.js + React Query で作るフルスタックアプリケーションの新時代

VSCode 開発なら Syntax Highlight のため先に Prisma 拡張 入れておくとよい。

$ npm i -D prisma       # 旧 @prisma/cli で init, migrate やらやるやつ
$ npm i @prisma/client  # こっちが実際に ORM として使う DB クライアント

# 構成ファイルの作成
#
# prisma/ スキーマの定義場所
# .env    DATABASE_URL 環境変数セット用、消して実行環境から入れてもいいはず
#         postgres の場合は ?schema=public まで入れないと migrate は通っても
#         ランタイムで Can't reach database server at で繋がらないことあった
#
$ npx prisma init

# schema の編集
#
$ vi prisma/schema.prisma

# DB migration (for dev)
# (schema 変更の sql 吐き出しと開発 DB への取り込み)
# 多分中で ↓ の prisma generate も一緒にやってる
#
$ npx prisma migrate dev --name first_migrate

# DB client ファイルの生成 (for prod)
# (schema 変更の @prisma/client へ向けた取り込み)
#
$ npx prisma generate

# DB migration (for prod)
# ↑ で吐き出し済みの sql を本番 DB へ流すだけのやつ
#
$ npx prisma migrate deploy

# Adminer みたいな DB の中身みる GUI 起動
#
$ npx prisma studio  # localhost:5555 で展開される

prisma generate

Generating the client

一般的な ORM と違い @prisma/clientschema.prisma に応じた DB client ファイルを prisma generate で都度生成し、それを import していることに注意。

  • client の実体は node_modules/.prisma/client に保存される
  • 別 dir へ指定もできるけど git 管理は非推奨みたい
  • あと @prisma/clientnpm i 時には自動生成されるらしい
    • ややこしい ... どうせ必要なら必ず叩くことにしてくれ

schema.prisma から CLI (prisma) で sql と client js を生成して @prisma/client で触るって感じなのかな。

で ... こういう特性を持っているため ↓ みたいなよくある最初に package.json だけ COPY する感じの Dockerfile だと npm i 時に prisma/schema.prisma を参照できなくて prisma generate を内部で実行できず、結果 Error: @prisma/client did not initialize yet となって DB 接続できないので注意。

FROM node:14.15.4

EXPOSE 80
WORKDIR /app

# npm install の build step を全体 copy と分けて
# cache し build 時間を短縮させるよくあるやつ
#
# https://qiita.com/Mayumi_Pythonista/items/3bec2d15980c99503793
#
COPY package*.json ./

# schema.prisma がないので prisma generate されない
RUN npm ci 

COPY . ./

# ここで生成してやる必要がある
RUN npx prisma generate

# ↑ で client 生成してからじゃないと build 通らん
# (node_modules にモノがないので)
RUN npm run build

CMD ["npm", "start"]

new PrismaClient

あと new PrismaClient() のたびに db connection 発生するらしい (未検証) ので、インスタンス使い回す感じにするのがいいらしい。

warn(prisma-client) Already 10 Prisma Clients are actively runningの対処
Best practice for instantiating Prisma Client with Next.js

// libs/prisma.ts

/* eslint-disable no-unused-vars */
/* eslint-disable no-var */

// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
import { PrismaClient } from '@prisma/client'
import { createUserExtends } from 'usecase/extends/create-user-extends'

const prismaClientSingleton = () => {
  return new PrismaClient({ log: ['info', 'warn', 'error'] })
    .$extends(createUserExtends)
}

declare global {
  var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>
}

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()

export { prisma }

if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
import { prisma } from 'libs/prisma'

const users = prisma.user.findMany()

補足として import のパス解決で相対パスにしたくない場合は ↓ のように構成してやればいい。

Next.jsで絶対パスを使う方法

// こんな directory architecture のとき
//
// ./
//   tsconfig.json
//   libs/
//     - prisma.ts
//   pages/
//   public/
//   styles/
{
  "compilerOptions": {
    //
    // baseUrl 指定して root directory 起点に
    // import {} from 'libs/prisma' とかできる
    //
    "baseUrl": ".",
    // ...
  }
}

prisma-repl

prisma-repl

prisma 向け REPL (Read Eval Print Loop) ライブラリ、いわゆる GOD 。ただ seeding の項目で書いた ts-node x module:nodenext の組み合わせでそのうちいらなくなる気がする。

$ npm i -D prisma-repl

$ npx prisma-repl
> await db.user.count()

# Override database_url 
$ npx prisma-repl --url postgres://xxx

Migrations

#
# よくある migration 関連コマンド
#
$ npx prisma migrate status  # マイグレーション状態の確認
$ npx prisma migrate reset   # 開発専用、全消し => 再構築 => seeding

# 開発環境専用のコマンド
# マイグレーションファイルの作成 => マイグレーション実行までやる
# prisma/schema.prisma 変更検知して prisma/migrations/ の
# 配下に 20210606142525_hoge.sql みたいなの生成してるぽい
#
$ npx prisma migrate dev --name ${MIGRATION_NAME}

# 本番環境専用のコマンド
# マイグレーションファイルの存在確認 => マイグレーション実行だけやる
# migrate dev で生成した .sql を DB に適用する本番専用のやつ
#
$ npx prisma migrate deploy

Shadow Database

Shadow database

あんまちゃんと理解してないけど ... Prisma Migrate は migrate devmigrate reset など開発時の DB 操作コマンドで tmp な DB を作る。

ので、開発環境で利用する DB では CREATE DATABASE などの権限を持つユーザで DB に接続しないといけない。

開発環境で利用する DB において (Heroku PostgreSQL を開発用に使うなどしてて) 上記権限ユーザを持てない場合は ↓ のように SHADOW_DATABASE_URL 環境変数セットするなどしてもう 1 個同じ構成の DB を用意しろ、ということらしい。

datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // これ
}

開発環境で Docker なんかで PostgreSQL コンテナ立ててつないでたりして、DB 全体の権限持ちユーザが使えるならいらない。

あと本番環境では migrate devmigrate reset は使わないので、特に気にせんでよい。

Seeding

Seeding your database

  • TypeStrong/ts-node ちゃんで .ts ファイルに書いた prisma script を実行するだけ
  • 「中で upsert なり組めばそれがもう seeding やろ?」って感じ、潔い ...
  • package.json > prisma.seedts-node prisma/seed.ts とかかけば npx prisma db seed でそれ実行したるっていうよくわからん配慮
  • 中で project の依存解決したいなら ts-node と dividab/tsconfig-paths セットで使うのがよき

prisma migrate reset

$ npm install -D typescript ts-node @types/node tsconfig-paths
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",  // import パス解決
    "paths": { /* */ },
  },
  //
  // ts-node 用の設定を追加
  // https://github.com/TypeStrong/ts-node#via-tsconfigjson-recommended
  //
  "ts-node": {
    //
    // ts-node からの実行時に baseUrl, paths とか解決してもらう
    // https://github.com/TypeStrong/ts-node#paths-and-baseurl
    //
    "require": ["tsconfig-paths/register"],
    "compilerOptions": {
      //
      // module は NodeNext とかにしとくと import とかいい感じにしてくれる
      // このあたり死ぬほどややこしいので ↓ ざっと読むとよき
      // https://zenn.dev/teppeis/articles/2021-10-typescript-45-esm
      //
      "module": "NodeNext",
      //
      // 実行環境の node.js ちゃんが読める形に落とす
      // ts-node の top level await 対応も考えると
      // node.js v14.15.x とあわせてこの辺がいいのでは
      //
      "target": "es2018",
    }
  },
  // ...
}
// package.json
{
  // これを set すると prisma migrate reset または
  // prisma migrate dev 時に db rest とともに
  // 自動実行されるらしい (開発環境用 CI ってコト!?)
  //
  // 開発中に今回の migrate dev で開発用の initialize
  // 用の seed が壊れてねえよなぁ!? ってことが検知できる利点ある
  //
  // 本番環境での実行は原則行わないような立て付けなんだと思う
  //
  "prisma": {
     "seed": "ts-node prisma/seed.ts"
  },
}
// prisma/seed.ts

import { Prisma, PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  const alice: Prisma.UserCreateInput = {
    email: '[email protected]',
    name: 'Alice',
  }

  await prisma.user.upsert({
    where: { email: alice.email },
    update: data,  // insert only なら update: {} とか
    create: data,
  })
}

main()
  .catch((e) => {
    console.error(e)
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })
# ts-node で ↑ prisma/seed.ts 実行
$ npx prisma db seed

Using tsx instead of ts-node

prisma/prisma#12752 (comment)

memory の少ない本番環境では ts-node だと killed されたりする。そんなときは tsx 使うほうがいい。

$ npx tsx prisma/some-seeds.ts

Seeding for CI to dev/prod

How to update many related records? #7547
Provide upsertMany operation in the client API #4134

seeding は prod において冪等性を捨てたほうがいいかも。

upsertMany みたいなものはまだない。ので、冪等な seeding を組みたいなら結構考えて独自に組まないといけない。(というか prisma の seed 機能は現状 data 操作の batch file でしかない)

そもそも現行 DB と seed file の diff をとって冪等に ... というアプローチ自体が、そこそこ大きなデータセットで破綻してしまう気がする (CI のたびに大量の Bulk update ... とかどの程度耐えられるのか) 。

とはいえ、殆ど変更されない & 変更に大きなコストが伴うような少量のマスターデータで、外部キー制約などの利点を生かさずとも管理しきれるものなら定数で管理する方向もある。またはファイルに逃がして stream する、別サービスとして API 連携する ... など。しかし、定数はオンメモリなので大量データに向かない、ファイルは I/O 負荷懸念、API は通信コストや異常系懸念などそれぞれ一長一短あり、どうしても DB に持たせたいケースは出てくる。

上記考慮し seeding は開発環境のみ自動実行 (CI) 前提 として、本番環境では冪等性のある自動実行は行わなず、手動実行する時系列ベースの seed file を運用する のが良さそうというのが、一旦の持論。

// package.json
{
  // prisma migrate reset 後に最低限の seed を insert だけする seed 
  // prisma db seed コマンドは基本 CLI で自分で叩かない想定
  //
  "prisma": {
    "seed": "ts-node prisma/local-seed.ts"
    //
    // これを set すると prisma migrate reset または
    // prisma migrate dev 時に db rest とともに
    // 自動実行されるらしい (開発環境用 CI ってコト!?)
    //
    // 開発中に今回の migrate dev で開発用の initialize
    // 用の seed が壊れてねえよなぁ!? ってことが検知できる利点ある
    //
  },
  "scripts": {
    //
    // ↑ のやつ、明示的 dev 環境で CLI で叩くならっていうやつ
    //
    "seed:dev": "prisma db seed",
    //
    // prod (本番) 環境で batch 的に手動実行する seed
    // npm run seed:prod --file=hoge.ts みたいな
    //
    "seed:prod": "ts-node prisma/seeds/$npm_config_file"
  }
}
// 多分こんな感じ
prisma/
  |
  ├ migrations/
  |  └ 20210101_init/
  |
  ├ seeds/
  | ├ 20220101-add-admins.ts
  | ├ 20220201-add-prefectures.ts
  | ├ 20220301-adhoc-update-admin-users.ts
  | └ 20220401-update-prefecture-18.ts
  |   //
  |   // ↑ では必ず $transaction を張った中で
  |   // (create|update|delete)(Many)? で構成し
  |   // upsert などによる冪等性の担保は「やらない」
  |   //
  |   // 本番では CI に組み込まず batch 的に手動実行する
  |   // 実行済み seed の重複実行だけ (別途管理するなどして) 注意する
  |
  └ local-seed.ts
    //
    // migrate reset で走らせて ↑ seeds/ から
    // adhoc-*.ts じゃないやつだけを抽出して
    // 「ざっくり本番に近しい初期状態」にする seeder
    //
    // 多分そこそこ大規模な開発になると、こいつの中身は
    // 本番 DB のマスターをマスク & データ量調整した
    // ステージング DB からの dump & bulk insert になる
// prisma/seeds/20220401-update-prefecture-18.ts

import { prisma } from 'libs/prisma'

export async function main() {
  //...
}

// 単体でも実行可能にしておく
main()
  .catch((e) => {
    console.error(e)
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })
// prisma/local-seed.ts

import { prisma } from 'libs/prisma'

// local の db reset に必要なやつだけ import して順次実行
import { main as addAdmins } from 'prisma/seeds/20220101-add-admins'
import { main as addPrefectures } from 'prisma/seeds/20220201-add-prefectures'
import { main as updatePrefecture18 } from '20220401-update-prefecture-18'

async function main() {
  await addAdmins()
  await addPrefectures()
  await updatePrefecture18()
}

main()
  .catch((e) => {
    console.error(e)
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })
# 開発 (dev) 環境では migrate reset で
# drop DB & migrate & seeding やりなおし
# (prisma db seed 経由で local-seed.ts 実行)
#
$ npx prisma migrate reset

# 本番 prod) 環境ではファイル指定して batch 的に
# prisma/seeds/*.ts を CI による deploy 後に実行
#
$ npm run seed:prod --file=20220401-update-prefecture-18.ts

Schema

Concepts / Components > Prisma schema
Reference / API reference > Prisma schema reference

  • npx prisma format で自動整形してくれる
  • schema 変更後は migration の他に prisma generate で client 生成・更新が必要
    • npm i や開発環境での prisma migrate dev では併せてやってくれる
    • Dockerfile などで「最初に COPY package*.json RUN npm ci する」ようなケースではその後 schema ファイルの COPY してから改めて generate かけないといけないので注意
  • client 生成・更新後は prisma.user.count() のように client が使えるだけでなく UserCreateInput のような型の import も行える
datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean  @default(false)
  title     String   @db.VarChar(255)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}

enum Role {
  USER
  ADMIN
}

Enum

// import して利用する場所によって type としても value としても使えるぽい
import { Role } from '@prisma/client'

const user = {
  role: Role.USER // value - 'USER'
}
type UserProfile = {
  role: Role // type - 'USER' | 'ADMIN'
}

Generated Types

Create a single record using generated types

import { PrismaClient, Prisma } from '@prisma/client'

// user model の生成時の parameter を type 検証できる
const user: Prisma.UserCreateInput = {
  email: '[email protected]',
  name: 'Elsa Prisma',
  posts: {
    create: {
      title: 'Include this post!',
    },
  },
}

const prisma = new PrismaClient()
const createUser = await prisma.user.create({ data: user })

@relation

Concepts / Components / Prisma schema > Relations

Multiple relation

Disambiguating relations

同じ model と複数の relation を貼りたいときは name をつけて従属側に専用の id attr を生やしてやる。

model User {
  id           Int     @id @default(autoincrement())
  name         String?
  writtenPosts Post[]  @relation("WrittenPosts")
  pinnedPost   Post?   @relation("PinnedPost")
}

model Post {
  id         Int     @id @default(autoincrement())
  title      String?
  author     User    @relation("WrittenPosts", fields: [authorId], references: [id])
  authorId   Int
  pinnedBy   User?   @relation(name: "PinnedPost", fields: [pinnedById], references: [id])
  pinnedById Int?
}

Json Field

Working with Json Fields

前提、多用すべきじゃないが json field も対応してる。

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["filterJson"]  // preview 機能で filter も可能
}

model User {
  // ...
  pets Json?
}
const json = [
  { name: 'Bob the dog' },
  { name: 'Claudine the cat' },
] as Prisma.JsonArray

// writing
const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    // js object から json への変換は自動でやってくれる
    pets: json,
  },
})

// reading
if (user?.pets && typeof user?.pets === 'object' && Array.isArray(user?.pets)) {
  const petsObject = user?.pets as Prisma.JsonArray

  const firstPet = petsObject[0]  // { name: 'Bob the dog' }
}

// filtering (全体一致)
var json = { [{ name: 'Bob the dog' }, { name: 'Claudine the cat' }] }

const getUsers = await prisma.user.findMany({
  where: {
    extendedPetsData: {
      equals: json,
    },
  },
})

// filtering (パス指定 & 値チェック)
const getUsers = await prisma.user.findMany({
  where: {
    extendedPetsData: {
      path: ['petType'],  // ['pet2', 'petName'] とか階層表現したり
      string_contains: 'cat',  // array_contains: ['Claudine'] とかもある
    },
  },
})

GetPayload Type

https://www.prisma.io/docs/orm/prisma-client/type-safety/operating-against-partial-structures-of-model-types#solution

↓ のように「 include で relation を保持している entity の型」が生成できる。

type UserWithPosts = Prisma.UserGetPayload<{
  include: {
    posts: true
  }
}>

With Prisma.validator

Prisma.validator
Operating against partial structures of your model types
Mapped Types - typescriptbook.jp

  • 定義済みの schema から型バリデーションを生成できる
  • User & { posts: Post[] } みたいなリレーション含めた型定義を動的に組むことも可能
  • 必ずワンセットで DB から引っ張るような model では特に便利
import { Prisma } from '@prisma/client'

// validator を使って型バリデーション object を生成
// こいつは find() とかの入力値に satisfies とかして型補完・型エラー検出させるためのもの
export const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
  include: { posts: true },
  // ここで ↓ みたいに required field を指定したり、省くべきやつ指定したりも
  select: {
    email: true,
    name: true,
    encrypted_password: false,
    posts: { title: true, body: true, published: true }
  },
})

// ↑ の User & { posts: Post[] } みたいな select, relation 定義から型を生成
// こいつは DB 返却に satisfies とか、backend コードで利用する感じのはず
export type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>

Prisma.PromiseReturnType

Problem: Getting access to the return type of a function

使い回さないような定義なら ↑ より、prisma の query や mutation 関数書いた後にこいつで export するほうが楽。

async function getUsersWithPosts() {
  const users = await prisma.user.findMany({ include: { posts: true } })
  return users
}

export type UsersWithPosts = Prisma.PromiseReturnType<typeof getUsersWithPosts>

Casting Date to (ISO) String by Mapped Types

JS Date オブジェクトではなく ISO 日時文字列を返す #5522

↑ とあわせて Date 型などを override をした serialized modal 型を作ると api 連携しやすい。

/**
 * ↑ に更に mapped types utility で再帰的に Date => string cast した
 * 「 api 返却は posts 持ち user & 日付系ぜんぶ seriarize されてるやつやで」型を作る
 *
 * 理由
 * ===
 * next の api で res.status(200).json() とかしたとき iso string になる
 * また getServerSideProps から渡すときも Date 型は super-json とか使わないと
 * error になるので素直に back => front 時には JSON.stringify() してやり
 * front-end では back-end から受け取ったデータをこの型で解決して引き回したいから
 */
export type UserWithPostsSd = WithSerializedDates<UserWithPosts>

// ↓ 再帰的に date => string  cast する util

/**
 * Utility Type of Serialized Prisma Model.
 * (Date => string)
 *
 * @link https://github.com/prisma/prisma/discussions/5522#discussioncomment-4630647
 */
export type WithSerializedDates<Type> = {
  [Key in keyof Type]: Type[Key] extends Date
  ? string
  : Type[Key] extends Date | null
  ? string | null
  : Type[Key] extends Date | undefined
  ? string | undefined
  : Type[Key] extends Date | null | undefined
  ? string | null | undefined
  : Type[Key] extends Record<PropertyKey, unknown>
  ? WithSerializedDates<Type[Key]>
  : Type[Key] extends Record<PropertyKey, unknown> | null
  ? WithSerializedDates<Type[Key]> | null
  : Type[Key] extends Record<PropertyKey, unknown>[]
  ? WithSerializedDates<Type[Key][number]>[]
  : Type[Key]
}

export const castSerializable = <T>(data: T) => (
  JSON.parse(JSON.stringify(data)) as WithSerializedDates<T>
)
// front ではきっとこう使う
import type { UserWithPostsSd } from 'path/to/type'

const res = await fetch('/api/users')
const user: UserWithPostsSd = await res.json()

console.log(
  user.posts[0].published // ISO string format
)

Custom Validation with zod

Concepts / Components / Prisma Client > Custom Validation
Add runtime validation to models #3528

prisma は schema に対する type validation しか責務として持っていない。よって user input に対する runtime validation は手前で好きな lib 使って対処しろとのこと。

ざっと調べた感じ zod の schema を schema.prisma から自動生成する zod-prisma の組み合わせが楽そう。

TypeScriptのゾッとする話 ~ Zodの紹介 ~

$ npm i zod
$ npm i -D zod-prisma
// schema.prisma

// ...

generator zod {
  provider = "zod-prisma"

  // 生成する zod schema の出力先
  // 利用時に import するので tsconfig.json で path 解決するなり
  // next.config.js で alias 指定するなりしてあげるといい
  //
  output   = "./libs/zod"
}

model Post {
  // とくに何も指定しなければ prisma を解析して
  // zod schema を自動生成してくれる
  //
  // ↓ なら id: z.string().uuid() とかになる
  //
  id String @id @default(uuid())

  // こんな風に @zod rich comment で詳細指定もできる
  //
  contents String /// @zod.max(10240)
  //
  // 上に書いてもよき
  //
  /// @zod.max(255, { message: "The title must be shorter than 256 characters" })
  title String
}
# ↑ schema の変更後に prisma generate する
$ npx prisma generate
// ↑ の prisma generate でこんな zod schema が
// output で指定した dir へ生成される
//
// こいつを .gitignore するか ... それとも登録するか悩ましいが
// 全部を schema.prisma に書ききる & パターンを統一させるのはしんどいので
// 基本的には zod-prisma は boilerplate 生成ツールとして割り切って
// この zod schema 定義をガリガリ更新していくほうがいいかなぁって印象
//
export const PostModel = z.object({
  id: z.string().uuid(),
  contents: z.string().max(10240),
  title: z.string().max(255, { message: 'The title must be shorter than 256 characters' }),
})
// ↑ で生成した zod schema を import 
import { UserModel } from 'libs/zod'  // path は適当に解決してね
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

try {
  // zod schema で入力値を validation してやる
  const data = UserModel.parse({ /* おそとからのにゅうりょく */ })

  // ↑ で ZodError にならなければ生成
  // このとき data は既に typed な状態になってるらしい
  //
  const createdUser = await prisma.user.create({ data })

} catch(e) {

  // .parse() の検証に失敗すると ZodError になるので
  // あとは catch した例外処理でお好きに異常系を組んでやる
  //
  console.error(err)

  /*
    ZodError の中身はこんな感じ、この一連の処理を formik やら
    react-final-form やらの validate function として登録、
    このエラーを wrap して返却して ... みたいなことする
    [
      {
        "code": "invalid_type", // エラータイプ
        "expected": "string",   // 期待した型
        "received": "number",   // 受け取った値の型
        "path": [ "name" ],      // エラーが発生したプロパティへのパス
        "message": "Expected string, received number" // エラー内容
      }
    ]
  */
}

react-final-form との連携例

Field errors helper method #93

イメージこんな。

import { Form } from 'react-final-form'

<Form
  validate={(values) => {
    try {
      UserModel.parse(values)
    } catch (e) {
      return e.formErrors.fieldErrors
    }
  }}
></Form>

毎回 try catch 書くのだるいなら wrapper かませとのこと。

import { z } from 'zod'

export const validator = <T extends z.ZodType<any,any>>(schema:T)=>(values:any)=>{
  try {
    schema.parse(values)
    return {}
  } catch (e) {
    return (e as z.ZodError).formErrors.fieldErrors
  }
}
import { validator } from 'libs/zod'
import { UserModel } from 'libs/zod'
import { Form } from 'react-final-form'

<Form validate={validator(UserModel)} />

Query

Concepts / Components / Prisma Client > CRUD
Concepts / Components / Prisma Client > Relation Queries - リレーション系はこちら

findUnique

schema.prisma@id@unique など一意性の定義をしたものを指定して検索。

rejectOnNotFound option を有効化しないと、存在しない場合に exception したりせずに null を返す挙動。

// By unique identifier
const user = await prisma.user.findUnique({
  where: {
    email: '[email protected]',
  },
})

// By ID
const user = await prisma.user.findUnique({
  where: {
    id: 99,
  },
  rejectOnNotFound: true,  // こいつ指定すれば not found 時に error になる
})

// By multiple key (@@id or @@unique)
//
// model TimePeriod {
//   year    Int
//   quarter Int
//   total   Decimal
// 
//   @@id([year, quarter])  こいつ
// }
//
const timePeriod = await prisma.timePeriod.findUnique({
  where: {
    year_quarter: {  // こんな一意性の指定ができる
      quarter: 4,
      year: 2020,
    },
  },
})

// with SELECT
const user = await prisma.user.findUnique({
  where: {
    email: '[email protected]',
  },
  select: {
    email: true,
    name: true,
  },
})

findFirst

こちらは一意じゃなくて OK な「最初のやつ返す」系。存在しない場合は default で null 返却。

findUnique 同様に rejectOnNotFound 指定で NotFoundError 吐かせられる。

const user = await prisma.user.findFirst({
  where: { name: 'Alice' },
  rejectOnNotFound: true,  // 存在しない場合は NotFoundError
})

// OR, AND (NOT もあるよ)
const users = await prisma.user.findFirst({
  where: {
    OR: [
      {
        name: {
          startsWith: 'E',
        },
      },
      {
        AND: {
          profileViews: {
            gt: 0,
          },
          role: {
            equals: 'ADMIN',
          },
        },
      },
    ],
  },
})

// WHERE by relations
const users = await prisma.user.findFirst({
  where: {
    email: {
      endsWith: 'example.com'
    },
    posts: {
      some: {
        published: false
      }
    }
  },
}

findMany

// SELECT * FROM users
const allUsers = await prisma.user.findMany()

// LEFT JOIN
const allUsers = await prisma.user.findMany({
  include: { posts: true },
})

// WHERE OR
const filteredPosts = await prisma.post.findMany({
  where: {
    OR: [
      { title: { contains: 'prisma' } },
      { content: { contains: 'prisma' } },
    ],
  },
})

create

// INSERT
const user = await prisma.user.create({
  data: {
    name: 'bob',
    email: '[email protected]',
    posts: {
      create: { title: 'bob\'s first post' },
    },
  },
})

createMany

多分 Bluk Insert 扱い、今んとこ relation の同時生成はサポート外。

const createdCount = await prisma.user.createMany({
  data: [
    { name: 'Bob', email: '[email protected]' },
    { name: 'Bobo', email: '[email protected]' }, // Duplicate unique key!
    { name: 'Yewande', email: '[email protected]' },
    { name: 'Angelique', email: '[email protected]' },
  ],
  skipDuplicates: true, // Skip 'Bobo'
})

update

// UPDATE
const post = await prisma.post.update({
  where: { id: 42 },
  data: { published: true },
})

upsert

Update or Create Records

const upsertUser = await prisma.user.upsert({
  where: {
    email: '[email protected]',
  },
  update: {
    name: 'Viola the Magnificent',
  },
  create: {
    email: '[email protected]',
    name: 'Viola the Magnificent',
  },
})

delete

単一削除、1 件以上あるとエラー。

// DELETE FROM xxx WHERE id = xxx
const deleteUser = await prisma.user.delete({
  where: {
    email: '[email protected]',
  },
})

deleteMany

複数削除ならこっち。

// DELETE FROM xxx WHERE xxx
const deleteUsers = await prisma.user.deleteMany({
  where: {
    email: {
      contains: 'prisma.io',
    },
  },
})

select

Select fields
select - api-reference

原則、find 系の query の返却 object は ↓ の状態になっている。

  • model の全 field を含む
  • relation を含まない

自身あるいは関連 relation の field 絞り込みは select で行い、relation の field 絞り込みを行わずに全 field 取得する際は後述の include を使う、という立て付け。

内部的には全体的に LEFT OUTER JOIN を使ってるみたいで、INNER JOIN するには工夫が要る気がする。

const users = await prisma.user.findMany({
  select: {
    name: true,
    posts: {
      // posts の left outer join
      select: {
        title: true,
      },
    },
  },
})

// {
//   "name": "Sabelle",
//   "posts": [
//      {
//         "title":"Getting started with Azure Functions"
//      },
//      {
//         "title":"All about databases"
//      }
//   ]
// }

↑ のような select による relation の nest は 親 model に対して select をかける ときしか使えないことに注意。

const getUser = await prisma.user.findUnique({
  where: {
    id: 1,
  },
  // select: { name: true } <-- NG
  // こんな感じで include と併せて個別に select 指定とかはできません
  include: {
    posts: {
      select: {
        title: true,
      },
    },
  },
})

include

Relation queries
include - api-reference

まるっと left join たのんますってやつ。prisma で関連 relation を取得する手法はだいたい ↓ 3 パターンで、この include を使うのが最も手軽。

  • この include で指定する
  • select の nest で指定する
  • 後述の where + fluent api で後から別 query 叩く
const getUser = await prisma.user.findUnique({
  where: { id: 19 },
  include: { posts: true },
})

// nest するなら
const user = await prisma.user.findMany({
  include: {
    posts: {
      include: {
        categories: true,
      },
    },
  },
})

// where で join 条件とか
// left なので条件一致な relation が「あれば一緒に」取得
// なくても親 model は取得する (rails だと inner join メインなので混乱する...)
const result = await prisma.user.findMany({
  include: {
    posts: {
      where: { published: true },
    },
  },
})

where

where - api-reference
Filtering and sorting

// OR や NOT (普通に条件追加すると AND 扱い)
const result = await prisma.user.findMany({
  where: {
    OR: [
      { email: { endsWith: 'prisma.io' } },
      { email: { endsWith: 'gmail.com' } },
    ],
    NOT: {
      email: { endsWith: 'hotmail.com' },
    },
  },
})

// relation field による絞り込み
const result = await prisma.post.findMany({
  where: {
    published: false,
    user: {
      email: { contains: 'prisma.io' },
    },
  },
})

// gt lt とか 
const result = await prisma.user.findMany({
  where: {
    posts: {
      some: {
        views: {
          gt: 10,
        },
      },
    },
  },
})

Filter operators

Filter conditions and operators

カラムに対する INNOT gt, lt とかの filter operation はこんな感じで指定。

const result = await prisma.user.findMany({
  where: {
    id: {
      in: [22, 91, 14, 2, 5],    // in
      notIn: [23, 92, 15, 3, 6]  // not in
    },
    name: { not: 'Eleanor' },    // not
    email: {
      contains: 'test',          // contains (for string)
      endsWith: '@example.com',  // endsWith (startsWith)
    },
    stars: { lt: 9 },            // lt (lte, gt, gte)
  },
})

none / some

prisma ちゃんは left join メインなので inner join 的な「持ってないやつは結果から省く」が言いづらい。

relation の count による filtering は未対応 だが、一応「少なくとも 1 件あるやつだけ来い」は where と some で言える。多分 inner join だと思う。

// 少なくとも 1 つの posts を持つ users カモン
const usersWithSomePosts = await prisma.user.findMany({
  where: {
    posts: { some: {} },
  },
})

// 1 つも posts 持ってない users カモン
const usersWithSomePosts = await prisma.user.findMany({
  where: {
    posts: { none: {} },
  },
})

is / isNot

Is there any way to filter by whether a relation exists or not? #2772

prisma ちゃんは (略) 1 対 1 の「持ってない」も表現しづらい。

上記 none / some は hasMany (one-to-many) 関係でしか使えない ため **hasOne (one-to-one) 関係では is null で表現してやる。

// OAuth とかで認証してて Account を持っている users カモン
const usersHasAccount = await prisma.user.findMany({
  where: {
    account: { isNot: null },
  },
})

// Email とかで認証してて Account 持ってない users カモン
const usersHasNoAccount = await prisma.user.findMany({
  where: {
    account: { is: null },
  },
})

where + include

prisma ちゃんは (略)「特定条件の relation を持っているやつ、その relation と一緒にカモン」が言いづらい。

include の where と親 model の where は独立しているので、このケースの場合は以下のように親 model 側と relation 側に同じ意味の where 条件を食わせてやるらしい。

// - published post 保持 user だけ post と一緒にほしい
// - include where だけだと published post のない user も一緒に来ちゃう
// - だったら user に published post もってるやつだけカモンって言っちゃう
//
const result = await prisma.user.findMany({
  where: {
    posts: {
      some: { published: true },
    },
  },
  include: {
    posts: {
      where: { published: true },
    },
  },
})

Fluent API

Fluent API

where で絞り込んだ結果から relation 集合を持ってくる、逆参照的な感じ。

// user 絞り込んで、そいつらの posts カモン
const postsByUser: Post[] = await prisma.user
  .findUnique({ where: { email: '[email protected]' } })
  .posts()

// ↑ と等価、こちらの方が 1 SQL で済む利点がある
// ただ graphql 的にはこっちのが batching が効いていいらしい
const postsByUser = await prisma.post.findMany({
  where: { author: { email: '[email protected]' } },
})

Pagination

Pagination

const results = await prisma.post.findMany({
  skip: 30,
  take: 10,
  where: {
    email: {
      contains: 'Prisma',
    },
  },
  orderBy: {
    title: 'desc',
  },
})

Full-text Search

Full-text search

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["fullTextSearch"]
}
// All posts that contain the words 'cat' or 'dog'.
const result = await prisma.posts.findMany({
  where: {
    body: {
      search: 'cat | dog',
    },
  },
})

// All drafts that contain the words 'cat' and 'dog'.
const result = await prisma.posts.findMany({
  where: {
    status: 'Draft',
    body: {
      search: 'cat & dog',
    },
  },
})

aggregate

Aggregation, grouping, and summarizing
aggregate - api-reference

  • _avg _sum _min _max _count などで field 指定
  • _count 以外は null 値許容
    • _count は 0 返却、それ以外は集計除外 + 全て null なら null 返却
  • 返却 object は ._avg.field みたいな感じで集計結果にアクセス可能
// 全女性 user のうち若さ上位 10 名の年齢平均が知りたい
const aggregations = await prisma.user.aggregate({
  _avg: { age: true },  // _sum, _min, _max, _count 利用可能
  where: { gender: 'FEMALE' },
  orderBy: { age: 'asc' },
  take: 10,
})

console.log('Average age:' + aggregations._avg.age)

count + select

// user の posts 数カモン
const usersWithCount = await prisma.user.findMany({
  include: {
    _count: { select: { posts: true } },
  },
})
// { id: 1, _count: { posts: 3 } },
// { id: 2, _count: { posts: 0 } },
// { id: 3, _count: { posts: 2 } },

// count + _all でレコード件数と null field 件数の比較とか
const userCount = await prisma.user.count({
  select: {
    _all: true, // Count all records
    id:   true, // Count all non-null field values
  },
})
// { _all: 30, name: 10 }

groupBy + having

// スウェーデンを除く user の profile view 数を country 別で集計
// 但し profile view 数 100 以上の user に限る
const groupUsers = await prisma.user.groupBy({
  by: ['country'],
  where: { country: { notIn: ['Sweden', 'Ghana'] } },
  _sum: { profileViews: true },
  having: {
    profileViews: { _avg: { gt: 100 } },
  },
})

distinct

const result = await prisma.user.findMany({
  where: {},
  distinct: ['name'],
})

orderBy

// relation つき
const posts = await prisma.post.findMany({
  orderBy: {
    author: { name: 'asc' },
  },
})

// count 結果で
const getActiveusers = await prisma.user.findMany({
  orderBy: {
    posts: { count: 'desc' },
  },
})

// multiple order
const users = await prisma.user.findMany({
  select: {
    email: true,
    role: true,
  },
  orderBy: [
    { email: 'desc' },
    { role: 'desc' },
  ],
})

// postgres v3.5 以上の full text search で関連語句検索的な
const posts = await prisma.post.findMany({
  orderBy: {
    _relevance: {
      fields: ['title'],
      search: 'database',
      sort: 'asc'
    },
})

$transaction

Interactive Transactions

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function transfer(from: string, to: string, amount: number) {
  return await prisma.$transaction(async (tx) => {
    // 1. Decrement amount from the sender.
    const sender = await tx.account.update({
      data: { balance: { decrement: amount } },
      where: { email: from },
    })

    // 2. Verify that the sender's balance didn't go below zero.
    if (sender.balance < 0) {
      throw new Error(`${from} doesn't have enough to send ${amount}`)
    }

    // 3. Increment the recipient's balance by amount
    const recipient = tx.account.update({
      data: { balance: { increment: amount } },
      where: { email: to },
    })

    return recipient
  })
}

async function main() {
  // This transfer is successful
  await transfer('[email protected]', '[email protected]', 100)
  // This transfer fails because Alice doesn't have enough funds in her account
  await transfer('[email protected]', '[email protected]', 100)
}

$queryRaw & Prisma.sql

$queryRaw

  • prepared statement つきの生 sql を書きたいときのやつ
  • generics で返り値の型指定もできる
  • 集計系かどうかによらず、返り値は必ず [] になってるので注意
  • mutation 系は $executeRaw を使う
import { Prisma, User } from '@prisma/client'

const domain = 'example.com'
const users = await prisma.$queryRaw<User[]>(
  Prisma.sql`SELECT * FROM User WHERE email like '%@${domain}'`
)

Prisma.join

import { Prisma } from '@prisma/client'

const ids = [1, 3, 5, 10, 20]
const result = await prisma.$queryRaw`SELECT * FROM User WHERE id IN (${Prisma.join(ids)})`

Middleware

Middleware
Session data middleware

  • PrismaClient インスタンスの生成後に $use() で hook を仕掛けられる
  • 生成箇所 1 つにしておいて export して全体で引き回すみたいな感じにしないといけなそう
  • model 毎、とかはまだないので if で分岐作るしかない
  • とはいえ model みて fluentd とかログサーバに飛ばしたりとかはできそう
import { Prisma } from '@prisma/client'

export const createPostMiddleware: Prisma.Middleware = async (params, next) => {
  if (params.model === 'Post' && params.action === 'create') {
    params.args.data.language = params.args.data.user.language
  }

  return next(params)
}
import { PrismaClient } from '@prisma/client'
import { createPostMiddleware } from './middlewares'

const prisma = new PrismaClient()

// middleware 登録
prisma.$use(createPostMiddleware)

export prisma // 全体でこいつを import して引き回す

params.args.data とかからの型ヒントなくなるうえに、usecase 毎にクエリ分けているなら「本当の意味で全体にかけたい」みたいなことって意外とない気がする。

ある動線からは「正」だけど、違う動線からは別の動きをしたいとか結構あるので、導入の前によく考える。本当に middleware としての使い方しかないんじゃないかな。

Extension

https://www.prisma.io/docs/orm/prisma-client/client-extensions
prisma/prisma#19725

いつのまにか middleware が非推奨になって extension でなんとかしろってことになっとる。

import { Prisma } from '@prisma/client'

export const createUserExtends = Prisma.defineExtension({
  query: {
    user: {
      async create ({ args, query }) {
        if (!args.data.color) {
          args.data = {
            ...args.data,
            color: 'orange',
          }
        }

        return query(args)
      },
    }
  }
})

export const prisma = new PrismaClient().$extends(createUserExtends)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment