はじめに
WebAPIの仕様としてリクエスト時に与えられたパラメータに応じて、WHERE句を動的に生成するケース(検索など)はよくあるが、それをPrismaで型安全に実装する方法について。
環境
- TypeScript:4.7.4
- Prisma:4.8.0
- Provider:MySQL
テーブル定義
usersテーブルに名前(name)と年齢(age)がカラム定義されている。
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 */
}
シンプルな実装例
findMany
のwhere
キーの値を直接指定するようなケースではこんな感じでシンプルかつ型安全に実装できる。
※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
型のため型安全ではなく、以下のような問題が考えられる。
- ヒューマンエラーによりカラム名や
StringFilter
、IntFilter
などで定義されているキー名(contains、gte、lteなど)および値の型定義に誤りがあると例外が発生する状態となる -
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'.
}
}
※カラム名を変更した場合や、StringFilter
、IntFilter
などで定義されているキー名(contains、gte、lteなど)および値の型定義に誤りがある場合も同様
まとめ
PrismaでWhere条件を動的に実装する必要があり、一度変数に格納するような場合には必ずPrismaClientから型定義情報をタイプインポートしany
型の使用を避けましょう。
参考