Kyselyによる型安全なSQLクエリ:肥大化したORMに代わる軽量な選択肢

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

生のSQLと重いORMの完璧な中間地点

Node.jsのバックエンド開発を新しく始める際、通常は生のSQLか重いORMかの選択を迫られます。生のSQL文字列は最高のパフォーマンスと制御性を提供しますが、非常に脆弱です。

カラム名のタイポ一つが、本番環境でクラッシュするまで検出されないこともあります。一方で、PrismaやTypeORMのようなORMは安全性を提供しますが、大きなオーバーヘッドを伴います。例えばPrismaのクエリエンジンは、Dockerイメージに100MB以上を追加し、内部の変換レイヤーだけでリクエストあたり5〜10msのレイテンシを発生させることがあります。

Kyselyは、その完璧な中間地点に位置します。これは、SQLを隠すのではなく活用する、TypeScriptファーストのクエリビルダーです。複雑な抽象化の代わりに、壊れたクエリを書くことをほぼ不可能にする型安全なレイヤーを提供します。私のチームでは、高い安定性を実現するためにこれを何年も本番環境で使用しています。従来のORMでデバッグを困難にしていた「魔法」なしに、コンパイル時のチェックという安心感を得られます。

Kyselyは、TypeScriptの高度な型システムを利用して、データベーススキーマをクエリに直接マッピングします。存在しないカラムを選択したり、テーブルの結合を間違えたりすると、コンパイラが即座にエラーを指摘します。オンコールの当番中に午前3時にバグを見つけるのではなく、開発中にバグをキャッチできるのです。

インストール:基盤の設定

開始するには、コアライブラリと特定のデータベース用のドライバーが必要です。ほとんどのプロジェクトでPostgreSQLを使用していますが、ロジックはMySQLやSQLiteでも同じです。まず、環境にTypeScriptが設定されている必要があります。

プロジェクトを初期化し、必要なパッケージをインストールします:

npm init -y
npm install kysely pg
npm install -D typescript @types/node @types/pg ts-node

Kyselyはクエリの構築を処理しますが、実際の接続は専用のドライバーに任せます。pgパッケージはPostgreSQLの接続プールを管理します。MySQLを好む場合は、pgmysql2に置き換えるだけです。ローカル開発や小規模なエッジ関数には、better-sqlite3が優れた選択肢となります。

手動でのインターフェース作成は面倒です。時間を節約するために、kysely-codegenを使用しましょう。このツールは稼働中のスキーマを検査し、TypeScriptの定義を自動的に生成するため、コードが常にデータベースの状態と一致することを保証します。

npm install -D kysely-codegen

設定:スキーマのマッピング

Databaseインターフェースは、アプリケーションの唯一の真実のソース(Single Source of Truth)として機能します。システム内のすべてのテーブル、カラム、データ型を定義します。このインターフェースが設定されると、Kyselyは記述するすべてのクエリに対して完全なIDEの自動補完を提供します。

接続を初期化するためにdb.tsファイルを作成します。usersテーブルとpostsテーブルを持つ標準的なブログ設定を見てみましょう。

import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';

interface UserTable {
  id: number;
  email: string;
  first_name: string;
  created_at: Date;
}

interface PostTable {
  id: number;
  title: string;
  content: string;
  author_id: number;
}

interface Database {
  users: UserTable;
  posts: PostTable;
}

export const db = new Kysely<Database>({
  dialect: new PostgresDialect({
    pool: new Pool({
      database: 'my_blog_db',
      host: 'localhost',
      user: 'admin',
      port: 5432,
      max: 10, // 最大10個の同時接続を維持する
    }),
  }),
});

KyselyコンストラクタにDatabaseインターフェースを渡すことで、型が実行エンジンにリンクされます。これ以降、dbオブジェクトはスキーマを完全に認識します。存在しないテーブルを参照させることはありません。

クエリの記述と安定性の確保

この設定のメリットは、入力を始めた瞬間に明らかになります。db.selectFrom('...')を呼び出すと、IDEがusersまたはpostsを候補として提案します。これにより、生の文字列クエリによくある「推測」が不要になります。

型安全なデータ取得

テーブルの結合は、ORMが混乱を招きやすい部分です。Kyselyは、厳格な型を維持しながら、構文を標準的なSQLに近づけています:

async function getAuthorWithPosts(userId: number) {
  const result = await db
    .selectFrom('users')
    .innerJoin('posts', 'posts.author_id', 'users.id')
    .select([
      'users.email',
      'posts.title',
      'posts.content'
    ])
    .where('users.id', '=', userId)
    .execute();

  return result;
}

データベースでemailcontact_emailにリネームしたとしましょう。Kyselyがない場合、遠く離れたファイルにある参照を見落とす可能性があります。Kyselyがあれば、コンパイラが即座にこのクエリをエラーとしてマークし、実行時の失敗を防ぎます。

データの挿入と更新

データの変更も同様に安全です。Kyselyは、挿入するオブジェクトが期待されるカラムの型と一致することを検証します。誤って整数カラムに文字列を送信してしまうのを防ぎます。

async function createPost(title: string, content: string, authorId: number) {
  return await db
    .insertInto('posts')
    .values({
      title,
      content,
      author_id: authorId,
    })
    .returning('id')
    .executeTakeFirst();
}

監視とデバッグ

抽象化は、内部で実際に何が起こっているかを隠してしまいがちです。Kyselyは、シンプルなロギングフックを提供することでこれを回避します。クエリの効率を確認するために、開発環境でこれを有効にすることをお勧めします。

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ ... }),
  log(event) {
    if (event.level === 'query') {
      console.log(`実行されたSQL: ${event.query.sql}`);
      console.log(`パラメータ: ${event.query.parameters}`);
    }
  },
});

これにより、サーバーに送信された正確なSQLを確認できます。クエリが遅い場合は、出力を直接pgAdminDBeaverに貼り付けることができます。独自のORM構文からSQLに翻訳し直すことなく、EXPLAIN ANALYZEを実行できます。

自信を持ってリファクタリングする

アプリケーションの成長に伴い、スキーマの変更は避けられません。Kyselyを使用していれば、リファクタリングはもはやリスクの高い作業ではありません。インターフェースのカラム型を更新すると、プロジェクト全体の壊れた参照がすべてエディタ上で赤く表示されます。コードがテスト環境に到達する前に、すべてのクエリを修正できます。このワークフローにより、私のチームは何百時間もの手動デバッグを節約し、本番環境でのエラー率を大幅に削減できました。

Share: