⚡️

Prisma TypedSQL をクエリビルダとしてのみ運用する

2024/08/29に公開

タイトルのこともできることを検証した。(ORM で文句ない人は ORM として使えばいい)

Prisma は TypeScript の優秀な ORM / QueryBuilder だが、Prisma 以外で運用されていると途中から投入するのが(一応可能ではあるが)面倒だったりする。

だが Typed SQL によって、既に存在するDBに対して、副作用なくクエリビルダとしてのみ導入することができるのでは、と思いついて試したところ、できた。

https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/typedsql

今回はリモートの 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