Prisma TypedSQL をクエリビルダとしてのみ運用する
タイトルのこともできることを検証した。(ORM で文句ない人は ORM として使えばいい)
Prisma は TypeScript の優秀な ORM / QueryBuilder だが、Prisma 以外で運用されていると途中から投入するのが(一応可能ではあるが)面倒だったりする。
だが Typed SQL によって、既に存在するDBに対して、副作用なくクエリビルダとしてのみ導入することができるのでは、と思いついて試したところ、できた。
今回はリモートの Supabase の PostgreSQL に対して行ったが、たぶん他の環境にも使える。
d1 とか。
prisma の最小プロジェクトのセットアップ
$ mkdir prisma-qb-only
$ cd prisma-qb-only
## 初期化
$ pnpm init
$ pnpm add prisma @prisma/client typescript -D
リモートからスキーマを取得
既にあるDBの定義から prisma/schema.prisma を組み立てる。今回は最近作った Supabase から適当に引っ張ってきた。
$ pnpm prisma db pull
Prisma schema loaded from prisma/schema.prisma
Environment variables loaded from .env
Datasource "db": PostgreSQL database "postgres", schema "public" at "aws-0-ap-northeast-1.pooler.supabase.com:5432"
✔ Introspected 1 model and wrote it into prisma/schema.prisma in 479ms
Run prisma generate to generate Prisma Client.
このようになった。
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters", "typedSql"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
model Post {
id String @id @default(cuid())
title String
content String
}
実行環境によるが、今回はこの DB に対してSQLを組み立てる。
(もしリモートDBがなくて単に query builder としてのみ試したい場合、この モデル定義で prisma migrate dev を行えば以降の結果はおなじになる。)
prisma/sql/addPost.sql から TS 関数を生成
まずは簡単なSQLで試す。
INSERT INTO "Post" (id, title, content) VALUES ($1, $2, $3);
これに対して、 prisma client の型の生成を行う
$ pnpm prisma generate --sql --no-engine
--no-engine
によって prisma-engine のクエリ実行エンジンを外す。
この generate 時に SQL に対する型チェックが行わる。未定義のモデルに対する操作等もエラーになる。嬉しいね。
これを import して実行するコードを書く。
// run.ts
import type { TypedSql } from "@prisma/client/runtime/library";
import { addPost } from "@prisma/client/sql";
const id = Math.random().toString(36).substring(7);
const query = addPost(id, "Title", "Content");
console.log(query.sql, query.values);
// TypedSql 型から結果の型を取り出すヘルパ
type TypedSqlToResultType<T> = T extends TypedSql<any, infer R> ? R : never;
// 実行後の型
type PostedResultType = TypedSqlToResultType<typeof query>;
クエリビルダとして使って、実行は行っていないことに注意。
せっかくなので最近サポートされた Node.js(v22) の TS 直接実行モードで実行してみる
$ node --experimental-strip-types --no-warnings=ExperimentalWarning run.ts
INSERT INTO "Post" (id, title, content) VALUES ($1, $2, $3); [ 'yubi48', 'Title', 'Content' ]
生の query と values が取り出せた。
ここまで prisma db pull 以外はクエリビルダとしてのみ使っているので、あとは任意の実行環境でこれを実行すればおk。
中身を確認しつつビルド
中身が気になる人向け。prisma generate --sql --no-engine
の吐き出すコードを確認する
node_modules/.pnpm/@prisma+client@5.19.0_prisma@5.19.0/node_modules/.prisma/client
みたいなパスに、実体が書き出されている。
import { makeTypedQueryFactory as $mkFactory } from "@prisma/client/runtime/library"
export const addPost = /*#__PURE__*/ $mkFactory("INSERT INTO \"Post\" (id, title, content) VALUES ($1, $2, $3);")
構成
$ tree node_modules/.pnpm/@prisma+client@5.19.0_prisma@5.19.0/node_modules/.prisma/client
node_modules/.pnpm/@prisma+client@5.19.0_prisma@5.19.0/node_modules/.prisma/client
├── default.d.ts
├── default.js
├── edge.d.ts
├── edge.js
├── index-browser.js
├── index.d.ts
├── index.js
├── package.json
├── schema.prisma
├── sql
│ ├── addPost.d.ts
│ ├── addPost.edge.js
│ ├── addPost.edge.mjs
│ ├── addPost.js
│ ├── addPost.mjs
│ ├── index.d.ts
│ ├── index.edge.js
│ ├── index.edge.mjs
│ ├── index.js
│ └── index.mjs
├── wasm.d.ts
└── wasm.js
node(cjs), edge, wasm 等のコードが吐かれている。
今回は素朴に node 用にさっき使った run.ts をバンドルしてみる。
$ npx esbuild --bundle run.ts --external:fs --external:os --external:events --external:async_hooks --external:util --external:child_process --external:path --external:tty > bundle.js
$ npx terser bundle.js -o bundle.min.js
$ ls -al bundle.js bundle.min.js
.rw-r--r-- 269k mizchi 29 Aug 17:13 bundle.js
.rw-r--r-- 192k mizchi 29 Aug 17:19 bundle.min.js
多少大きいが、Edge でカツカツの環境でない限りサーバーサイドで使う想定なので、問題になりづらいと思われる。
まとめ
今回のクエリぐらいだったら普通に Prisma を ORM として使った方が早いが、複雑なクエリをTypedSQL に倒す方法があるというのは嬉しい。
ただ、自分のSQL力が低くてエッジケースを踏みそうな複雑なクエリを作れてない。誰か頼む。
ORM 嫌いな人も生SQLを透過的に扱ってTS関数化する Typed SQL なら許容してくれそうな気がする。どうなんですかねみなさん。
Discussion