はじめに
TypeScriptでバックエンド開発をしていると、データベースアクセスの選択肢で悩むことが多いんですよね。
ORMを使えば型安全にはなるけど、複雑なクエリを書こうとすると途端に辛くなる。かといって生SQLを文字列で書くと、タイプミスに気づくのが実行時になってしまう。このジレンマ、30代エンジニアなら一度は経験したことがあるんじゃないでしょうか。
そこで今回紹介したいのがKysely(キーセリーと読みます)というライブラリです。
GitHubスター数は13.1k、npmの月間ダウンロード数は約550万件という実績。個人的には、TypeScriptでSQLを扱うなら現時点でベストな選択肢だと思っています。
Kyselyの特徴・メリット
1. 完全な型安全性
Kyselyの最大の特徴は、クエリのあらゆる部分で型チェックが効くことです。
- テーブル名やカラム名を間違えるとコンパイルエラー
- SELECTした結果の型が自動的に推論される
- JOINやサブクエリでも型情報が保持される
これ、意外と重要なんですよ。カラム名のタイポで本番障害とか、避けられるなら避けたいですよね。
2. SQLに近い直感的なAPI
ORMにありがちな「独自の書き方を覚える必要がある問題」がほとんどありません。SQLを知っていれば、ほぼそのままの感覚で書けます。
// これがKyselyのクエリ。SQLがそのまま読める
const users = await db
.selectFrom('users')
.where('status', '=', 'active')
.orderBy('created_at', 'desc')
.limit(10)
.selectAll()
.execute()
3. 幅広い対応環境
Node.jsだけでなく、Deno、Bun、Cloudflare Workers、さらにはブラウザでも動作します。対応データベースもPostgreSQL、MySQL、SQLite、Microsoft SQL Serverと主要どころは押さえています。
4. 軽量でシンプル
ORMのような重厚な抽象化レイヤーがないので、動作が軽快です。「SQLは書けるけど型安全にしたい」というニーズにピンポイントで応えてくれます。
インストール方法
npmでインストールします。データベースに応じたドライバも一緒に入れる必要があります。
# Kysely本体
npm install kysely
# PostgreSQLの場合
npm install pg
# MySQLの場合
npm install mysql2
# SQLiteの場合
npm install better-sqlite3
基本的な使い方
データベース型の定義
まず、データベースのスキーマをTypeScriptの型として定義します。ここが型安全性の源泉になります。
import { Generated, Insertable, Selectable, Updateable } from 'kysely'
// テーブルの型定義
interface UserTable {
id: Generated<number> // AUTO_INCREMENTなカラム
name: string
email: string
status: 'active' | 'inactive'
created_at: Generated<Date>
}
interface PostTable {
id: Generated<number>
user_id: number
title: string
content: string
published: boolean
}
// データベース全体の型
interface Database {
users: UserTable
posts: PostTable
}
// 操作用の型(便利なので定義しておく)
type User = Selectable<UserTable>
type NewUser = Insertable<UserTable>
type UserUpdate = Updateable<UserTable>
Kyselyインスタンスの作成
import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'
const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
host: 'localhost',
database: 'myapp',
user: 'postgres',
password: 'password',
})
})
})
CRUD操作の例
// SELECT - ユーザー一覧を取得
const users = await db
.selectFrom('users')
.where('status', '=', 'active')
.selectAll()
.execute()
// users の型は User[] と自動推論される
// SELECT - 1件取得
const user = await db
.selectFrom('users')
.where('id', '=', 1)
.selectAll()
.executeTakeFirst()
// user の型は User | undefined
// INSERT - 新規作成
const newUser = await db
.insertInto('users')
.values({
name: '山田太郎',
email: 'yamada@example.com',
status: 'active'
})
.returningAll()
.executeTakeFirstOrThrow()
// UPDATE - 更新
await db
.updateTable('users')
.set({ status: 'inactive' })
.where('id', '=', 1)
.execute()
// DELETE - 削除
await db
.deleteFrom('users')
.where('id', '=', 1)
.execute()
JOINも型安全
const postsWithUser = await db
.selectFrom('posts')
.innerJoin('users', 'users.id', 'posts.user_id')
.where('posts.published', '=', true)
.select([
'posts.id',
'posts.title',
'users.name as author_name' // エイリアスも型に反映される
])
.execute()
実践的なユースケース
リポジトリパターンでの利用
実際のプロジェクトでは、リポジトリパターンと組み合わせることが多いです。
class UserRepository {
constructor(private db: Kysely<Database>) {}
async findById(id: number): Promise<User | undefined> {
return this.db
.selectFrom('users')
.where('id', '=', id)
.selectAll()
.executeTakeFirst()
}
async findActiveUsers(): Promise<User[]> {
return this.db
.selectFrom('users')
.where('status', '=', 'active')
.orderBy('created_at', 'desc')
.selectAll()
.execute()
}
async create(user: NewUser): Promise<User> {
return this.db
.insertInto('users')
.values(user)
.returningAll()
.executeTakeFirstOrThrow()
}
async updateStatus(id: number, status: 'active' | 'inactive'): Promise<void> {
await this.db
.updateTable('users')
.set({ status })
.where('id', '=', id)
.execute()
}
}
動的な条件の構築
検索条件が動的に変わる場合も、型安全に書けます。
async function searchUsers(params: {
name?: string
status?: 'active' | 'inactive'
limit?: number
}) {
let query = db.selectFrom('users').selectAll()
if (params.name) {
query = query.where('name', 'like', `%${params.name}%`)
}
if (params.status) {
query = query.where('status', '=', params.status)
}
if (params.limit) {
query = query.limit(params.limit)
}
return query.execute()
}
トランザクション処理
await db.transaction().execute(async (trx) => {
const user = await trx
.insertInto('users')
.values({ name: '新規ユーザー', email: 'new@example.com', status: 'active' })
.returningAll()
.executeTakeFirstOrThrow()
await trx
.insertInto('posts')
.values({
user_id: user.id,
title: '最初の投稿',
content: 'よろしくお願いします',
published: true
})
.execute()
})
まとめ
Kyselyは「SQLは書けるけど型安全にしたい」というニーズに対する、現時点でのベストアンサーだと個人的には思っています。
正直なところ、Prismaのような本格的なORMと比べると機能は少ないです。マイグレーションツールは別途用意する必要がありますし、リレーションの自動解決みたいな機能もありません。
でも、その分シンプルで学習コストが低い。SQLを知っていれば即戦力で使えます。
こんな人には特におすすめですね。
- SQLは普通に書けるけど、文字列で書くのは不安
- ORMの学習コストや制約が煩わしい
- 複雑なクエリも柔軟に書きたい
- TypeScriptの型システムを最大限活用したい
興味があれば、公式サイト(kysely.dev)のドキュメントがかなり充実しているので、そちらも覗いてみてください。