生SQLに型を手書きする時代は終わり?Prismaの新機能「TypedSQL」
🧩

生SQLに型を手書きする時代は終わり?Prismaの新機能「TypedSQL」

2024/08/28に公開1

生SQLを扱う $queryRaw

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に対して自動で型を付けてくれる機能です。

https://www.prisma.io/blog/announcing-typedsql-make-your-raw-sql-queries-type-safe-with-prisma-orm

ざっくりとした仕組み

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.prismagenerator client にて、 previewFeature として typedSql を追加してあげます。

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["typedSql"]
}

2. sql/ に任意のSQLを書いたファイルを置く

schema.prisma が置かれているディレクトリに sql ディレクトリを作成し、その下に任意の .sql ファイルを作成します。例えば sql/listPostsWithAuthor.sql というファイルを置き、以下のSQLを実装します。

使用するスキーマ定義

schema.prismaには以下が定義されているとします。

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
}
listPostsWithAuthor.sql
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 にコードが生成されます。

4. $queryRawTyped を使う

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 では以下のような型定義ファイルが生成されていました。

listPostsWithAuthor.d.ts
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
  }
}

対応しているカラムの型について

現在ドキュメントはありませんが、以下のテストコードを見ると対応している型がわかりそうです。
https://github.com/prisma/prisma/tree/main/packages/client/tests/functional/typed-sql

PostgreSQLを見るとJSONB型やXML型など、かなり対応範囲は広そうです。

感想とか

TypedSQL、非常にありがたいです。最近 $queryRaw を書く頻度が上がっており、まさに欲しいと思っていた機能でした。どのくらい温めていた機能なのかはわかりませんが、Prismaの勢いのようなものを感じますね。特にPrisma Clientの約4,000行のクソデカPRがパワーを感じて良かったです(小並感)。
https://github.com/prisma/prisma/pull/24907

PrismaのTypedSQLはGoのsqlcとコンセプトが近そうです。ただしsqlcは本物のPostgreSQLのパーサを呼び出して型情報を取得していますが、Prismaは実際のDBとやりとりをして型情報を取得しているという違いがありそうです。

また、私は勝手に世はTypeScript ORMライブラリ戦国時代だと思っています。現在はPrismaが一歩リードしていますが、drizzleやkyselyなど、新たなコンセプトを持ったORMやクエリビルダが猛追してきているなぁと感じています。そんな中、今回のPrismaのTypedSQLは覇権を確固たるものにする起爆剤となるのでしょうか。今後の動きに期待が高まりますね。

Discussion