生SQLに型を手書きする時代は終わり?Prismaの新機能「TypedSQL」
$queryRaw
生SQLを扱う TypeScript向けのORMライブラリとしてPrismaがあります。Prismaは直感的で型安全なAPIを提供し、TypeScript向けのORMとしては第一に名前が上がることが多いライブラリです。
しかしそんな人気なPrismaでも、裏側では少しクセのあるSQLが発行されていたり、欲しいSQLがPrismaのAPIでは実現できない場合があります。
そういった場合のために $queryRaw
というメソッドが用意されており、これを使うことで生SQLを書いてその結果を受け取ることができました。他のORMにもよくある機能です。
例えば以下のように実装することができます。
const users = await prisma.$queryRaw`
SELECT id, name FROM "Users" WHERE id = ${userID}
`;
console.log(users) // [{ id: 1, name: "taro" }, { id: 2, name: "jiro" }]
しかしTypeScriptでこれを扱う上ではある課題があります。そう、 型 です。
$queryRaw
の結果が入るこの users
の型は残念ながら unknown
となってしまいます。
そこで $queryRaw
では以下のようにGenericsを用いて返ってくるレコードの型を指定する必要があります。
const users = await prisma.$queryRaw<{ id: number; name: string }[]>`
SELECT id, name FROM "Users" WHERE id = ${userID}
`;
これによって users
に型が付いてくれます。
しかし型を都度手作業で書くのは大変ですし、そもそも本当にこの型でレコードが返ってくるかは保証されていません。 as
を使っているのと同じ気持ちです。
そんな中、数時間前にPrismaがこの課題を解決する素晴らしい新機能をリリースしたので紹介したいと思います。
Prismaの新機能「TypedSQL」
Prisma 5.19より、Preview Featureとして「TypedSQL」という機能が実装されました。ざっくりと説明するとその名の通り 生SQLに対して自動で型を付けてくれる機能です。
ざっくりとした仕組み
Prismaは型安全なAPIを提供しますが、これは独自DSLで記述された schema.prisma
からTypeScriptの型定義を事前に生成し、それを参照することで実現しています。この型定義を生成するコマンドが prisma generate
です。
TypedSQLはこれと似た仕組みで実現されています。事前に型を付けたいSQLを書いてあげて、 prisma generate --sql
を実行することでSQLに対応するTypeScriptの型定義ファイル等を生成し、それを実装時に使うことで生SQLにも型が付くという仕組みです。
ちなみにこのSQLから型情報を抜く処理はprisma-engineというRust製のコンポーネントによって実装されていそうです。
使ってみる
1. previewFeatureの追加
TypedSQLは現在Preview Featureなため、 schema.prisma
の generator client
にて、 previewFeature
として typedSql
を追加してあげます。
generator client {
provider = "prisma-client-js"
previewFeatures = ["typedSql"]
}
sql/
に任意のSQLを書いたファイルを置く
2. schema.prisma
が置かれているディレクトリに sql
ディレクトリを作成し、その下に任意の .sql
ファイルを作成します。例えば sql/listPostsWithAuthor.sql
というファイルを置き、以下のSQLを実装します。
使用するスキーマ定義
schema.prismaには以下が定義されているとします。
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
SELECT
p.id,
p.title,
p.content,
u.id as "authorId",
u.name as "authorName"
FROM "Post" as p
INNER JOIN "User" as u ON u.id = p."authorId"
WHERE u.id = $1;
3. generate実行
以下を実行します。
prisma generate --sql
これにより、 node_modules/.prisma/client/sql
にコードが生成されます。
$queryRawTyped
を使う
4. 3で生成されたコードを、今回新たに実装された $queryRawTyped
メソッドを使って呼び出します。
import { listPostsWithAuthor } from "@prisma/client/sql"
const userId = 1;
const users = await prisma.$queryRawTyped(listPostsWithAuthor(userId));
おわかりいただけたでしょうか。 listPostsWithAuthor.sql
に対応するコードが listPostsWithUser
として生成されています。 またこのSQLでは u.id = $1
というパラメータが存在するため、 listPostsWithUser
は引数にnumberを取るシグネチャになっています。
そして users
にはちゃんと型が付いています。
このように生SQLを型安全に扱えることがわかりました。
ちなみに prisma generate --sql
では以下のような型定義ファイルが生成されていました。
import * as $runtime from "@prisma/client/runtime/library"
/**
* @param int4
*/
export const listPostsWithAuthor: (int4: number) => $runtime.TypedSql<listPostsWithAuthor.Parameters, listPostsWithAuthor.Result>
export namespace listPostsWithAuthor {
export type Parameters = [int4: number]
export type Result = {
id: number
title: string
content: string | null
authorId: number
authorName: string
}
}
対応しているカラムの型について
現在ドキュメントはありませんが、以下のテストコードを見ると対応している型がわかりそうです。
PostgreSQLを見るとJSONB型やXML型など、かなり対応範囲は広そうです。
感想とか
TypedSQL、非常にありがたいです。最近 $queryRaw
を書く頻度が上がっており、まさに欲しいと思っていた機能でした。どのくらい温めていた機能なのかはわかりませんが、Prismaの勢いのようなものを感じますね。特にPrisma Clientの約4,000行のクソデカPRがパワーを感じて良かったです(小並感)。
PrismaのTypedSQLはGoのsqlcとコンセプトが近そうです。ただしsqlcは本物のPostgreSQLのパーサを呼び出して型情報を取得していますが、Prismaは実際のDBとやりとりをして型情報を取得しているという違いがありそうです。
また、私は勝手に世はTypeScript ORMライブラリ戦国時代だと思っています。現在はPrismaが一歩リードしていますが、drizzleやkyselyなど、新たなコンセプトを持ったORMやクエリビルダが猛追してきているなぁと感じています。そんな中、今回のPrismaのTypedSQLは覇権を確固たるものにする起爆剤となるのでしょうか。今後の動きに期待が高まりますね。
Discussion
arigato