最近、Node.js用のORMを調べていました。下の画像はnpm trendsというnpmライブラリーのダウンロード数の履歴がわかるサイトでORMライブラリーを調べた時の画像です。以前記事を書いたPrismaなどに人気がある事がわかります。そして今回ネットを調べていたさいに知ったKyselyやZapatosのドキュメントを読んでいると、これらはORMではない新しいRDB用ライブラリーだとわかりました。
npm trends より
Kysely とは
DB接続ライブラリー vs ORM
従来、RDBを扱うプログラムを書こうとすると、以下の選択肢がありました。
ORMはちゃんとSQLを学ばなくても何とかコードが書けるのがメリットでした(RDBやSQLの知識が0では扱うのは難しいと思いますが・・・)。またRuby on RailsなどのようにORMを内蔵しているフレームワークが流行った事もあり、多くのプログラムが便利さを体験しました。
ただし、ORMには最適なSQL文が生成・実行されるとはかぎらず性能上の問題になる場合がある。またSQLの機能を使えばできる事がORMでは出来ない・難しい場合があるなどの問題もありました。
TypeScript中毒
TypeScriptに慣れすぎたプログラマーは、型が書かれてない(型推論できない)コードには不安を覚えるようになってしまいました。
ORMを使うと型の問題はORMがなんとかしてくれますが、DB接続ライブラリー + SQLの場合は
- ライブラリーからの戻り値やライブラリーに渡すデータ(オブジェクト)の型がテーブル定義に一致してなくても実行してみるまで判らない
- SQL文は文字列なのでキーワード等が間違っていても実行するまで判らない(VS Codeの拡張機能でチェックできるかな?)
- SQL文の中に書かれたテーブル名、カラム名が間違っていても実行するまで判らない、これはVS Codeではチェックできない(たぶん)
- また、VS Codeでコーディングするさいにテーブル名やカラム名の補完が出来ない(贅沢!?)
これらの問題を解決する事がKyselyの目標のように思えます。
Kysely を使ってみた
RailsプログラマーのためのPrisma入門(1), (2), (3)で使ったPostgreSQLのデータベースをKyselyを使ってアクセスしてみましょう。
型定義ファイル
Kyselyではテーブルの型定義ファイルを元に動きます、以下は今回のデータベースの定義です(一部省略しました)。
手動で書いてもよいのですが今回は既存のデータベースを使うのでkysely-codegenというツールを使い既存のデータベースから定義ファイルを作ってもらいました。
import type { ColumnType } from "kysely";
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export interface Cats {
country_id: number | null;
description: string | null;
id: Generated<number>;
name: string;
reference_image_id: string | null;
temperament: string | null;
}
export interface Countries {
country_code: string;
id: Generated<number>;
name: string;
}
export interface DB {
cats: Cats;
countries: Countries;
}
ちなみにKyselyはMigration機能をもっています。Ruby on Railsに似た雰囲気でSQL文ではなく独自APIを使います、ただし初期のRails同様にdown側も人間が書く必要があります。
簡単なSELECT文
SELECT * FROM cats ORDER BY name LIMIT 10
を実行するのには以下のようなコードを書きます。RailsのActiveRecord(Arel)と雰囲気が似ていますね。
- ① kysely-codegenが作成したテーブル定義を使っています
- ② データベース接続は各データベース毎の接続関数が用意されています
- ③ logオプションを指定しました。これで実行されるSQL文がコンソールに表示されます
- ④ SQL文の実行、ActiveRecord(Arel)のようにSQL文を組み立てる関数を組み合わせてSQL文を作ります
selectFrom
SQLのFORM テーブル
selectAll
SQLのSELECT *
orderBy
SQLのORDER BY
limit
SQLのLIMIT
execute
SQL文の実行- 関数の並び順には少し制限があるようです
- テーブル名やカラム名はVS Code上で補完できます!
- 戻り値はCatsテーブルの検索結果型*1の配列になります
mport { Insertable, Kysely, PostgresDialect } from 'kysely';
import { Cats, Countries, DB } from 'kysely-codegen'; // ← ①
import { Pool } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
const db = new Kysely<DB>({ // ← ②
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
}),
log: ['query', 'error'] // ← ③
});
(async () => {
const rows = await db.selectFrom('cats') // ← ④
.selectAll()
.orderBy('name')
.limit(10)
.execute();
console.log(rows);
})();
*1 Kyselyでは検索結果やINSERT文やUPDATE文に渡すデータの型が自動的に作成されます。これは便利ですね!
- 検索結果の型
Selectable<Cats>
type Cat = {
country_id: number | null;
description: string | null;
id: number;
name: string;
reference_image_id: string | null;
temperament: string | null;
}
- INSERT文のデータ型
Insertable<Cats>
type NewCat = {
name: string;
} & {
country_id?: number | null | undefined;
description?: string | null | undefined;
id?: number | undefined;
reference_image_id?: string | null | undefined;
temperament?: string | null | undefined;
}
- UPDATE文のデータ型
Updateable<Cats>
type CatUpdate = {
country_id?: number | null | undefined;
description?: string | null | undefined;
id?: number | undefined;
name?: string | undefined;
reference_image_id?: string | null | undefined;
temperament?: string | null | undefined;
}
Join
ORMではないので関連情報の取得はSQLのJOINやサブクエリーを使う必要があります。
.innerJoin(TABLE, KEY1, KEY2)
関数はSQLのJOIN テーブル名 ON KEY1 = KEY2
になりますinnerJoin
はselect
より先に書かないといけないようです
(async () => {
const rows = await db.selectFrom('cats')
.innerJoin('countries', 'cats.country_id', 'countries.id')
.select(['cats.id', 'cats.name', 'countries.name as country'])
.orderBy('cats.name')
.limit(10)
.execute();
console.log(rows);
})();
ここで、SELECT結果(rows)の型は、以下のようにちゃんと出来ています。
const rows: {
name: string;
id: number;
country: string;
}[]
SQLを直接書きたい
上のコードを見ていると「SQL文を直接書いた方が楽なのでは?」と思い調べてみると以下のようにsqlタグ関数(タグ付きテンプレート)を使うとSQL文が実行できます。
Kyselyの中ではsqlタグ関数はSQLの一部に式(例 sql`cats.name||cats.name`
)を書くためのもののようです。
(async () => {
const {rows} = await sql`SELECT * FROM cats ORDER BY name LIMIT 10`.execute(db);
console.log(rows);
})();
という事でJOINの例をSQLで書くと以下のようになります。
(async () => {
onst {rows} = await sql`
SELECT cats.id, cats.name, countries.name as country FROM cats
JOIN countries ON cats.country_id = countries.id
ORDER BY cats.name LIMIT 10
`.execute(db);
console.log(rows);
})();
しかし、SELECT結果(rows)の型は、const rows: unknown[]
になってしまいました。🥹
もちろん、以下のようにsqlの型パラメーターで指定すれば型は付きますが、CatWithCountryの定義が間違っていてもエラーは出ません。
(async () => {
type CatWithCountry = {
id: number;
name: string;
contry: string;
};
const {rows} = await sql<CatWithCountry>`
SELECT cats.id, cats.name, countries.name as country FROM cats
JOIN countries ON cats.country_id = countries.id
ORDER BY cats.name LIMIT 10
`.execute(db);
console.log(rows);
})();
まとめ
Kyselyが目指している事はわかった気がします。テーブル定義から型が作れるのは良いアプローチだと思います、またselectFrom
, innerJoin
…などの関数をVS Codeで書くさいにテーブル名、カラム名が補完されるのも素晴らしいですね。
ただし selectFrom
, innerJoin
…などの関数の組み合わせでSQLを書くのは私は好きではありません。
ReactのJSXが、JavaScriptの中にHTMLを書けるように拡張したように、JavaScriptの中にSQL文を書けるようなライブラリー・ツールが現れないでしょうか?