okayurisotto.net

私が好きでやったことが他の人のためにもなったらお得かも!

クエリビルダー「Kysely」からPGliteを使えるようにする

公開日:

導入:クエリビルダーを使う意義/Prismaを使わない理由

TypeScriptからデータベースを扱いたいとなったとき、おそらく多くの人はORMとしてPrismaを選ぶでしょう。PrismaはNode.js(とBun)に対応したORMで、その最大の特徴は型安全であること。Prisma登場以前によく使われていたORMとしてTypeORMというものがありますが(もちろん今でも使われていますよ)、少しでも高度なことをしようとすると型推論の効かない書き方をする必要が出てきて、正直使い勝手はよくありません。対するPrismaでは、独自の構文によるスキーマから生成された型定義で型安全にデータベースを扱えます。複雑なJOINもうまく動き、開発体験はとてもよいです。

しかしPrismaにもいくつかの欠点はあります。

例えば、クエリは少し特徴的な書き方をします。決してわかりにくいわけではなく、むしろ統一感があるためSQLよりわかりやすいと私には感じられますが、それでもSQLに慣れ親しんだ人にとっては書き心地はあまりよくないでしょう。また、実際に生成されるSQLがどういったものになるのか、この書き方からは想像できません。素直にSQLを書けるほうが楽な場面もあるでしょう。

const c = await prisma.user.create({
  data: { name: 'Alice', email: '[email protected]' },
});

const r = await prisma.user.findUnique({
  where: { email: '[email protected]' },
});

const u = await prisma.user.update({
  where: { email: '[email protected]' },
  data: { age: 20 },
});

const d = await prisma.user.delete({
  where: { email: '[email protected]' },
});

また、Prismaはたくさんの機能を持ったヘビーなライブラリです。動作環境としてはNode.jsが想定されていて、それ以外のランタイムとなるとBunでは動かせるかなといった具合。当然Webブラウザでは動きません。「そもそもWebブラウザでORMを使いたい場面はないだろう」と思われるかもしれませんが、実は最近のWebではそうとも限りません。SQLiteやPostgreSQLはWASMコンパイルされたものがすでに利用可能です。また、Webブラウザから使えるメモアプリのNotionは、WASM版SQLiteを活用することで一部の環境でのパフォーマンスを向上させました(解説記事)。

Webブラウザでもデータベースを使いたいという需要は確かにあって、残念ながらそういった場面ではPrismaは使えません。しかしだからといって、型安全でない生のSQLを書くことはしたくないものです。軽量で環境を問わず使えるORMのようなものはないでしょうか?

この記事で紹介するのは、Kyselyというクエリビルダーです。クエリビルダーとは、その名の通りSQLクエリを組み上げるもので、それ以上のものではありません。そのシンプルさから、Node.jsやBunに限らず、DenoやWebブラウザ、Cloudflare Workersでも動きます。そしてKyselyの書き心地は、「プログラミング言語のように書けるSQL」といった感じ。すでにSQLを知っている人にとってはとても書きやすいものでしょう。

const person = await db
  .selectFrom('user')
  .selectAll()
  .where('email', '=', '[email protected]')
  .executeTakeFirst();

もちろん型安全です。安心安全便利なプログラミングができます。

interface Database {
  user: {
    id: GeneratedAlways<number>;
    name: ColumnType<string>;
    email: ColumnType<string>;
  };
}

const db = new Kysely<Database>({ dialect: someDialect });

Prismaのエコシステムも便利なものですが、クエリビルダーも決して悪くありません。

本題:KyselyからPGliteを使ってみる

さて、そんなKyselyですが、「Dialect」という概念によって様々な種類のRDBMSに対応しています。PostgreSQLやMySQLやSQLiteはもちろん、コミュニティによるDialectではCloudflare D1やWASM版SQLiteやWASM版PostgreSQLにも対応しています。

この記事では、WASM版PostgreSQLであるPGliteをKyselyから使えるようにDialectを自作してみます。車輪の再発明ではありますが、KyselyのDialectの概念や、クエリビルダーがいかに軽量なものであるかがよくわかるため、教材として優れていると感じます。

Dialectとは。何を実装する必要がある?

KyselyのDialectとは、あるRDBMSを扱う上で必要になるそのRDBMS特有の処理を切り出したもので、次に示す4つの機能が集まったものです。

  • Driver
  • QueryCompiler
  • Introspector
  • Adapter

Driverは、データベースへのコネクション(DatabaseConnection)を管理する役割を担う概念で、単純なコネクションの作成や解放の他にも、コネクションプールの管理なども担います。

QueryCompilerはその名の通り、ツリー構造で渡された操作情報をSQLクエリの形にコンパイルする役割を担う概念です。compileQueryメソッドを持つクラスとして実装します。

Introspectorは、データベースがどういったテーブル・列を持っているのかといったメタデータを取得する役割を担う概念です。getTablesメソッドとgetSchemasメソッドを持ちます。

AdapterはDriverやQueryCompilerでは表現できないDialect間での差異を吸収するための概念です。「そのRDBMSがDDL命令のトランザクションに対応しているか?」や「INSERTなどでのreturningに対応しているか?」をプロパティとして持ちます。

今回作ろうとしているPGlite用Dialectで実装する必要があるのは、Driverだけです。というのも、他3つはすでにPostgreSQL用の公式Dialectに含まれているためです。PGliteはWASM版のPostgreSQLですから、そのまま流用できるわけです。また、PostgreSQL用の公式Dialectのソースコードを読んでいただければ分かる通り、Driverも特に難しい実装がされたものではありません。ですので実質、DatabaseConnectionさえ実装すればOKです。

PGliteConnectionの実装

きっと読んでもらったほうが早いです。

import type { DatabaseConnection, CompiledQuery, QueryResult } from "kysely";

interface PGliteDialectConfig {
  database: PGlite;
}

export class PGliteConnection implements DatabaseConnection {
  readonly #config;

  public constructor(config: PGliteDialectConfig) {
    this.#config = config;
  }

  public async executeQuery<R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> {
    const results = await this.#config.database.query(
      compiledQuery.sql,
      [...compiledQuery.parameters],
    );

    return {
      rows: results.rows as R[],
      ...(results.affectedRows
        ? { numAffectedRows: BigInt(results.affectedRows) }
        : {}),
    };
  }

  public async *streamQuery<R>(
    _compiledQuery: CompiledQuery,
    _chunkSize?: number
  ): AsyncIterableIterator<QueryResult<R>> {
    throw new Error(`PGliteDriver doesn't support streaming.`);
  }
}

読んでもらえば分かる通り、executeQueryメソッドにSQLとしてコンパイルされたクエリが渡されるので、それをPGliteのqueryメソッドに渡しているだけです。

おわりに

データベースを扱うライブラリとなると、どことなく難しそうでとっつきにくさを感じてしまいますが、少なくともKyselyのDialectに関してはとてもシンプルでわかりやすい仕組みをしていて、理解しやすかったのではないでしょうか。

Dialectにおいて最も複雑なのはおそらくQueryCompilerで、そこを既存の実装で流用できたのが最も大きな省力化だったでしょう。ちなみに公式のPostgreSQLのQueryCompilerの実装はこちらです。各種OperationNodeからなる木構造をRootOperationNodeから地道に走査していくような書かれ方をしています。なかなか大変そうですが、まぁSQL自体が大変な構文をしていますから仕方ないでしょう。

あとで時間があったらKysely本体も読んでみたいですね。流石にすぐに理解できるような文量ではありませんが、斜め読みしてみた感じでは、適切な粒度でclassやinterfaceに分けられたうえでimplementsされていて、私にもたくさんの時間を作って頑張って読めばギリギリ読めそうでした。