アプリケーション開発の観点から見たDrizzle ORMとKyselyの比較

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

TypeScriptでバックエンドサーバーを開発する際、適切なORMの選択は常に重要な決断の一つです。最近、私のプロジェクトでDrizzle ORMとKyselyの両方を試す機会がありましたが、個人的にはDrizzle ORMの方が便利で生産性が高かった経験を共有したいと思います。

2つのORMの簡単な紹介

Drizzle ORMはTypeScript用のORMで、型安全性と直感的なAPIを強みとしています。スキーマ定義からマイグレーション、クエリビルダーまでフルスタック開発体験を提供します。

Kyselyは「型安全なSQLクエリビルダー」と自己紹介しており、TypeScriptの型システムを活用してクエリ作成時の型安全性を保証します。

どちらのツールも優れていますが、私の開発経験に照らし合わせると、Drizzle ORMがいくつかの面でより便利でした。

Drizzle ORMを好むようになった理由

スキーマ定義の直感性

Drizzle ORMのスキーマ定義方法は非常に直感的で宣言的です:

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').unique().notNull(),
  age: integer('age')
});

Drizzle ORMはこのスキーマ定義から自動的にCREATE TABLE SQLを生成できるため、スキーマとコードが常に同期された状態を保てます。

一方、Kyselyは型定義により重点を置いており、スキーマと型定義が分離する傾向があります:

interface Database {
  users: {
    id: Generated<number>;
    name: string;
    email: string;
    age: number | null;
  };
}

この型定義はTypeScriptコードで型安全性を提供しますが、この型定義だけではCREATE TABLE SQLを生成できないことが決定的な欠点です。実際にテーブルを作成するには、別途SQLスクリプトやマイグレーションコードを作成する必要があります。これにより、型と実際のデータベーススキーマ間の不一致が生じる可能性が高まります。

DrizzleのアプローチはデータベーススキーマとTypeScriptの型をより緊密に連携させ、開発過程での混乱を減らしてくれました。

マイグレーション体験

Drizzle ORMのマイグレーションツール(drizzle-kit)は本当に印象的でした。スキーマの変更を自動的に検出してSQLマイグレーションファイルを生成する機能が開発ワークフローを大きく改善しました:

npx drizzle-kit generate:pg

このコマンド一つでスキーマ変更に対するマイグレーションファイルが生成され、それをレビューして適用するプロセスが非常に簡単でした。

一方、Kyselyのマイグレーションは本質的に手動です。開発者が直接マイグレーションファイルを作成する必要があり、スキーマ変更を自動的に検出したりSQLを生成したりする機能がありません:

// Kyselyのマイグレーション例
async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('name', 'text', (col) => col.notNull())
    .addColumn('email', 'text', (col) => col.unique().notNull())
    .addColumn('age', 'integer')
    .execute();
}

async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

この手動方式は複雑なスキーマ変更で間違いを犯す可能性が高くなり、特に大きなプロジェクトでは作業量がかなり増加する可能性がありました。

しかし、Kyselyのマイグレーションにも2つの重要な利点があります:

  1. TypeScriptベースのマイグレーション: KyselyのマイグレーションスクリプトはTypeScriptで記述されるため、マイグレーションロジックにアプリケーションロジックを統合できます。例えば、S3などのオブジェクトストレージのデータも一緒にマイグレートする複雑なシナリオを実装できます。一方、Drizzle ORMはSQLベースのマイグレーションなので、このような統合はできません。

  2. 双方向マイグレーション: Kyselyはupdown関数の両方を定義して、アップグレードとダウングレードの両方をサポートします。これは特にチーム協業環境で重要で、他の開発者の変更と競合が発生した場合にロールバックが必要になることがあるためです。Drizzle ORMは現在アップグレードのみをサポートしており、ダウングレード機能がないため、協業時に不便な場合があります。

なお、PythonエコシステムのSQLAlchemyマイグレーションツールであるAlembicは、さらに発展した形式のマイグレーションを提供しています。Alembicは非線形のマイグレーションパス(ブランチポイントの作成が可能)をサポートし、複雑なチーム開発環境でも柔軟に対応できます。理想的には、JavaScript/TypeScriptエコシステムのORMもこのレベルのマイグレーションツールを提供することが望ましいでしょう。

リレーション設定の容易さ

Drizzle ORMでのテーブル間のリレーション設定が非常に直感的でした:

import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ one, many }) => ({
  profile: one(profiles, {
    fields: [users.id],
    references: [profiles.userId],
  }),
  posts: many(posts)
}));

この方式はデータベース設計の本質的な、関係的な側面を明確に表現してくれました。

クエリ作成の利便性と同名カラムの問題処理

両方のORMともクエリ作成のためのAPIを提供していますが、Drizzleのアプローチの方がより直感的で関係モデルを活用しやすかったです:

// Drizzle ORM - db.queryの方式でリレーションを活用
const result = await db.query.posts.findMany({
  where: eq(posts.published, true),
  with: {
    user: true  // 投稿作成者情報を一緒に取得
  }
});

// 結果へのアクセスが直感的で型安全
console.log(result[0].title);       // 投稿タイトル
console.log(result[0].user.name);   // 作成者名 - オブジェクト構造で明確に区別される
console.log(result[0].user.id);     // 作成者ID - 投稿IDと名前が同じでも問題ない

// Kysely
const result = await db
  .selectFrom('posts')
  .where('posts.published', '=', true)
  .leftJoin('users', 'posts.userId', 'users.id')
  .selectAll();

// 結果アクセス時のカラム名衝突問題
console.log(result[0].id) // エラー: posts.idとusers.idのどちらなのか曖昧
console.log(result[0].name) // エラー: 両方にnameカラムがあれば曖昧

Drizzleのアプローチはテーブルとカラムを参照する際の型安全性をより強力に保証し、リレーションを活用したクエリ作成がより直感的でした。

特に複数テーブル結合時の同名カラムの処理の面でDrizzle ORMがはるかに便利でした。これは私の開発経験において最も重要な違いの一つでした。

// Drizzle ORM - 同名カラムの処理
const result = await db.query.posts.findMany({
  with: {
    user: true  // posts.idとusers.idの両方があるが自動的に区別される
  }
});

// 結果に自然にアクセス可能
console.log(result[0].id);        // 投稿ID
console.log(result[0].user.id);   // ユーザーID - 明確に区別される
console.log(result[0].user.name); // ユーザー名

// Kysely - 同名カラム処理のためにエイリアスが必要
const result = await db
  .selectFrom('posts')
  .leftJoin('users', 'posts.userId', 'users.id')
  .select([
    'posts.id as postId',       // エイリアス必須
    'posts.title',
    'posts.content',
    'users.id as userId',       // エイリアス必須
    'users.name as userName',   // カラム名が同じ可能性があるためエイリアス必須
    'users.email as userEmail'  // 一貫性のためにすべてのユーザー関連カラムに接頭辞が必要
  ]);

// エイリアスを通じたアクセス
console.log(result[0].postId);    // 投稿ID
console.log(result[0].userId);    // ユーザーID
console.log(result[0].userName);  // ユーザー名

Drizzle ORMはテーブルとカラムをオブジェクトとして参照するため、同じ名前のカラムがあっても自然に階層構造で処理され、型推論も正確に機能します。一方、Kyselyでは文字列ベースのアプローチのため、エイリアスを手動で指定する必要がある場合が多く、複雑な結合でこの作業が煩雑になりました。特に複数のテーブルに同じ名前のカラムが多いほど、すべてのカラムに明示的なエイリアスを指定する不便さがありました。

またDrizzle ORMは結果の型を自動的に正確に推論してくれるため、別途の型指定なしでも安全に結果を使用できました。

Kyselyの長所

もちろんKyselyにも多くの強みがあります:

  1. より軽量な構造:必要な機能だけを含めることができるモジュール化された構造
  2. SQLにより近いアプローチ:SQL構文に非常に忠実なAPI設計
  3. 柔軟性:複雑なクエリで時にはより柔軟な記述が可能

また前述したように、KyselyのTypeScriptベースのマイグレーションと双方向(up/down)マイグレーションのサポートは、特定の状況ではDrizzle ORMより優位にある機能です。

SQLAlchemyとの比較および今後の期待

JavaScript/TypeScriptエコシステムのORMについて話す前に、様々な言語の中でもPythonのSQLAlchemyは特別な位置を占めています。個人的に今まで使ってきた様々な言語のORMの中で、SQLAlchemyが最も機能が豊富で強力だと感じました。複雑なクエリ構成、高度なリレーションマッピング、トランザクション管理、イベントシステムなど、SQLAlchemyの機能は本当に膨大です。

Drizzle ORMはJavaScriptエコシステムで非常に印象的な発展を遂げましたが、まだSQLAlchemyの境地には達していないと思います。特に次のような点でSQLAlchemyの成熟度と機能の豊富さが際立っています:

  • 複雑なサブクエリとウィンドウ関数のサポート
  • 多様なイベントリスナーとフック
  • 様々な継承戦略
  • 複雑なトランザクション管理とセッション管理
  • 大規模プロジェクトで検証された安定性
  • Alembicを通じた非線形マイグレーションのサポート
  • 驚くほど膨大で詳細なドキュメント

結論

両方のORMとも優れたツールですが、私の開発スタイルとプロジェクト要件にはDrizzle ORMの方がより適していました。特にスキーマ定義の直感性、強力なマイグレーションツール、そして全体的な開発者体験の面でDrizzle ORMがより生産的な開発を可能にしてくれました。

同名カラム処理のような実質的な問題においてDrizzle ORMのオブジェクトベースのアプローチがもたらす利便性は、実際のプロジェクトで大きな違いを生み出しました。

ORMの選択は結局のところプロジェクトの特性と個人の好みに大きく左右されます。新しいプロジェクトを始めるなら、両方のツールを簡単にテストしてみて、自分のワークフローにより適したものを選ぶのが良いでしょうが、私の場合はDrizzle ORMが明確な勝者でした。

今後、Drizzle ORMがさらに発展してSQLAlchemyレベルの豊富な機能と柔軟性を提供するようになることを願っています。JavaScript/TypeScriptエコシステムにもそのレベルの強力なORMがあれば良いと思います。幸いにもDrizzle ORMは継続的に発展しており、その発展速度を見ると期待が大きいです。

皆さんの経験はいかがですか?他のORMツールや言語を使ったことがあれば、ぜひ意見を共有してください!

0

1 comment

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0195a271-d48e-7caf-aed3-f5eb020d5e63 on your instance and reply to it.

딱 ORM은 아니지만, 목적은 같은 하스켈의 opaleye 가 있습니다. 쿼리에 타입을 도입한 모양이라, 쿼리 검사에 타입 체커의 도움을 받을 수 있습니다. 처음 봤을 때, 함수형 방식으로 ORM 같은 걸 만든다면, 이렇게 되는 구나 감탄했는데, 익히는 비용이 만만치 않은 것 같습니다. @hongminhee洪 民憙 (Hong Minhee)

0