EY-Office ブログ

ORMとは違うRDBライブラリーが出てきたようです

最近、Node.js用のORMを調べていました。下の画像はnpm trendsというnpmライブラリーのダウンロード数の履歴がわかるサイトでORMライブラリーを調べた時の画像です。以前記事を書いたPrismaなどに人気がある事がわかります。そして今回ネットを調べていたさいに知ったKyselyZapatosのドキュメントを読んでいると、これらはORMではない新しいRDB用ライブラリーだとわかりました。

npm trends npm trends より

Kysely とは

DB接続ライブラリー vs ORM

従来、RDBを扱うプログラムを書こうとすると、以下の選択肢がありました。

  • pgのようなDB接続ライブラリーを使い、SQL文を書きライブラリーに渡す方式
  • PrismaのようなORMを使い、(SQL文を書かず)JavaScript/TypeScriptのみで書く方式

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になります
  • innerJoinselectより先に書かないといけないようです
(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文を書けるようなライブラリー・ツールが現れないでしょうか?

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ