8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PrismaでWhere条件を動的かつ型安全に実装する方法

Last updated at Posted at 2023-01-26

はじめに

WebAPIの仕様としてリクエスト時に与えられたパラメータに応じて、WHERE句を動的に生成するケース(検索など)はよくあるが、それをPrismaで型安全に実装する方法について。

環境

  • TypeScript:4.7.4
  • Prisma:4.8.0
  • Provider:MySQL

テーブル定義

usersテーブルに名前(name)と年齢(age)がカラム定義されている。

schema.prisma
model User {
  id   Int    @id @default(autoincrement()) @db.UnsignedInt
  name String
  age  Int

  @@map("users")
}

WebAPIとしての抽出条件の仕様

オプショナルで以下の条件が指定可能なユーザー一覧のWebAPIを想定する。

  • 名前(name)に特定の文字列が含まれているか?
  • 年齢(age)がn歳以上か?
  • 年齢(age)がn歳以下か?

参考実装用の仮想定義だがこのような型のオブジェクトが与えられるイメージ

type Args = {
  name?: string
  minAge?: number
  maxAge?: number
}
const args: Args = {
  /* optional */
}

シンプルな実装例

findManywhereキーの値を直接指定するようなケースではこんな感じでシンプルかつ型安全に実装できる。
undefinedの場合にはWHERE句には含まれない

const users = await this.prisma.user.findMany({
  where: {
    name: {
      contains: args.name
    },
    age: {
      gte: args.minAge,
      lte: args.maxAge
    }
  }
})

全て指定されていた場合に実際に発行されるQueryのWHERE句は以下の通り。

WHERE (`db`.`users`.`name` LIKE ? AND `db`.`users`.`age` >= ? AND `db`.`users`.`age` <= ?)

しかし、WHERE句の対象となるカラムが増えたり条件の制御が複雑になりだすと、whereキーの値を一度オブジェクト変数として定義して処理の中で追加するような実装が必要になることがある。

whereキーに指定する値をオブジェクト変数として定義し後から追加する実装例(not type-safe)

const where: any = {
  name: {
    contains: args.name
  },
  age: undefined
}

where.age = { gte: args.minAge, lte: args.maxAge }

const users = await this.prisma.user.findMany({
  where
})

あるいはオブジェクトのスプレッド構文を使って...

let where

where = {
  name: {
    contains: args.name
  }
}

where = {
  ...where,
  age: { gte: args.minAge, lte: args.maxAge }
}

const users = await this.prisma.user.findMany({
  where
})

結果として最初のシンプルな実装例と同様にQueryのWHERE句を動的に生成できるが、いずれもwhere変数はany型のため型安全ではなく、以下のような問題が考えられる。

  1. ヒューマンエラーによりカラム名やStringFilterIntFilterなどで定義されているキー名(contains、gte、lteなど)および値の型定義に誤りがあると例外が発生する状態となる
  2. whereで指定している対象カラムのカラム名や型定義を変更してしまうと例外が発生する状態となる

最大の問題は上記の状態でコンパイル(トランスパイル)が通ってしまうため、単体テストなどでしっかりとフォローできていないケースでは問題を検知できない可能性があること。

当然のことながらこれはwhereキーに値を直接指定している場合にはtsコンパイラがエラーを示してくれる。

const users = await this.prisma.user.findMany({
  where: {
    hoge: {
      contains: args.name // Type '{ hoge: { contains: string; }; }' is not assignable to type 'UserWhereInput'.
    }
  }
})

上記のエラーからも分かるとおり、このエラーはUserWhereInputタイプとの型の不一致により発生しているため、
where変数をUserWhereInputタイプで定義することで型安全に実装することができる。

型安全な実装例

PrismaClientのリファレンスを確認すると、examples-7のような例を参考にすると型安全に実装できることが分かる。

$ prisma generateによって生成されるPrismaClient(node_modules/.prisma/client)には{Model名}WhereInputが定義されているので、それをタイプインポートすることで適切な型定義を利用することができる。

import type { Prisma } from '@prisma/client'

const where: Prisma.UserWhereInput = {
  name: {
    contains: args.name
  }
  // 型情報にageキーが含まれているため事前にキーを宣言しundefinedとしておく必要もない
  // age: undefined
}

where.age = { gte: args.minAge, lte: args.maxAge }

const users = await this.prisma.user.findMany({
  where
})

上記のような実装としておくことで仮に存在しないカラムを指定した場合にはwhereキーに直接指定した場合と同様にtsコンパイラがエラーを示してくれるため、動的かつ型安全に実装することができる。

const where: Prisma.UserWhereInput = {
  hoge: {
    contains: args.name // Type '{ hoge: { contains: string; }; }' is not assignable to type 'UserWhereInput'.
  }
}

※カラム名を変更した場合や、StringFilterIntFilterなどで定義されているキー名(contains、gte、lteなど)および値の型定義に誤りがある場合も同様

まとめ

PrismaでWhere条件を動的に実装する必要があり、一度変数に格納するような場合には必ずPrismaClientから型定義情報をタイプインポートしany型の使用を避けましょう。

参考

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?