kysely の使い方

Type safe SQL query builder

v0.28.153.7M/週MITORM / DB
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Kysely の使い方 — TypeScript 型安全 SQL クエリビルダー完全ガイド

一言でいうと

Kysely(キーセリー)は、TypeScript の型システムを最大限に活用した SQL クエリビルダーです。テーブル名・カラム名・結果型をすべてコンパイル時に検証し、IDE の自動補完も完全に効く状態で SQL を組み立てられます。

どんな時に使う?

  • ORM は重すぎるが、生 SQL は型安全性がなくて不安 — SQL の書き心地を保ちつつ、型の恩恵を受けたいとき
  • 複雑なクエリを安全に組み立てたい — サブクエリ、JOIN、CTE(WITH句)などを多用するアプリケーションで、型の整合性を保証したいとき
  • マルチDB対応のバックエンドを構築したい — PostgreSQL、MySQL、SQLite、MS SQL Server など複数の DB に対応する必要があるとき

インストール

# npm
npm install kysely

# yarn
yarn add kysely

# pnpm
pnpm add kysely

DB ドライバーは別途インストールが必要です。

# PostgreSQL
npm install pg
# または
npm install postgres

# MySQL
npm install mysql2

# SQLite (better-sqlite3)
npm install better-sqlite3

# MS SQL Server
npm install tedious tarn

基本的な使い方

1. データベーススキーマの型定義

Kysely の型安全性の根幹は、データベースのスキーマを TypeScript の型として定義することにあります。

import {
  ColumnType,
  Generated,
  Insertable,
  Selectable,
  Updateable,
} from 'kysely'

// データベース全体の型定義
interface Database {
  person: PersonTable
  pet: PetTable
}

interface PersonTable {
  id: Generated<number>          // INSERT 時に省略可能(auto increment)
  first_name: string
  last_name: string | null
  created_at: ColumnType<Date, string | undefined, never>
  // ColumnType<SelectType, InsertType, UpdateType>
  // → SELECT 時は Date、INSERT 時は string | undefined、UPDATE 不可
}

interface PetTable {
  id: Generated<number>
  name: string
  owner_id: number
  species: 'dog' | 'cat' | 'hamster'
}

// ヘルパー型(INSERT/UPDATE 用)
type Person = Selectable<PersonTable>
type NewPerson = Insertable<PersonTable>
type PersonUpdate = Updateable<PersonTable>

2. Kysely インスタンスの作成

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

const db = new Kysely<Database>({
  dialect: new PostgresDialect({
    pool: new Pool({
      host: 'localhost',
      database: 'my_app',
      user: 'postgres',
      password: 'password',
    }),
  }),
})

3. クエリの実行

// SELECT
const persons = await db
  .selectFrom('person')
  .select(['id', 'first_name', 'last_name'])
  .where('first_name', '=', 'John')
  .execute()
// persons の型: { id: number; first_name: string; last_name: string | null }[]

// INSERT
const result = await db
  .insertInto('person')
  .values({
    first_name: 'Jane',
    last_name: 'Doe',
    created_at: new Date().toISOString(),
  })
  .returningAll()
  .executeTakeFirstOrThrow()

// UPDATE
await db
  .updateTable('person')
  .set({ last_name: 'Smith' })
  .where('id', '=', 1)
  .execute()

// DELETE
await db
  .deleteFrom('pet')
  .where('id', '=', 42)
  .execute()

よく使う API — Kysely の主要メソッドの使い方

1. selectFrom + where — 条件付き SELECT

// 基本的な WHERE
const adults = await db
  .selectFrom('person')
  .selectAll()
  .where('first_name', 'like', '%John%')
  .execute()

// 複数条件(AND)
const result = await db
  .selectFrom('person')
  .select(['id', 'first_name'])
  .where('first_name', '=', 'John')
  .where('last_name', 'is not', null)
  .execute()

// OR 条件・ネストした条件
const result2 = await db
  .selectFrom('person')
  .selectAll()
  .where((eb) =>
    eb.or([
      eb('first_name', '=', 'John'),
      eb.and([
        eb('first_name', '=', 'Jane'),
        eb('last_name', '=', 'Doe'),
      ]),
    ])
  )
  .execute()

2. innerJoin / leftJoin — テーブル結合

const petsWithOwners = await db
  .selectFrom('pet')
  .innerJoin('person', 'person.id', 'pet.owner_id')
  .select([
    'pet.name as pet_name',
    'person.first_name as owner_name',
    'pet.species',
  ])
  .execute()
// 型: { pet_name: string; owner_name: string; species: 'dog' | 'cat' | 'hamster' }[]

// LEFT JOIN の場合、結合先のカラムは自動的に nullable になる
const petsWithOptionalOwner = await db
  .selectFrom('pet')
  .leftJoin('person', 'person.id', 'pet.owner_id')
  .select(['pet.name', 'person.first_name'])
  .execute()
// person.first_name の型は string | null になる

3. insertInto — 一括挿入と RETURNING

// 複数行の一括挿入
await db
  .insertInto('pet')
  .values([
    { name: 'Buddy', owner_id: 1, species: 'dog' },
    { name: 'Whiskers', owner_id: 2, species: 'cat' },
  ])
  .execute()

// INSERT ... ON CONFLICT(UPSERT)— PostgreSQL
await db
  .insertInto('person')
  .values({
    id: 1,
    first_name: 'John',
    last_name: 'Doe',
  })
  .onConflict((oc) =>
    oc.column('id').doUpdateSet({
      first_name: 'John',
      last_name: 'Doe',
    })
  )
  .execute()

4. with — CTE(共通テーブル式)

const result = await db
  .with('dog_owners', (qb) =>
    qb
      .selectFrom('pet')
      .innerJoin('person', 'person.id', 'pet.owner_id')
      .where('pet.species', '=', 'dog')
      .select(['person.id', 'person.first_name'])
  )
  .selectFrom('dog_owners')
  .selectAll()
  .execute()
// CTE の結果型も正しく推論される

5. transaction — トランザクション

const result = await db.transaction().execute(async (trx) => {
  const person = await trx
    .insertInto('person')
    .values({
      first_name: 'Alice',
      last_name: 'Wonderland',
    })
    .returningAll()
    .executeTakeFirstOrThrow()

  await trx
    .insertInto('pet')
    .values({
      name: 'Cheshire',
      owner_id: person.id,
      species: 'cat',
    })
    .execute()

  return person
})
// トランザクション内でエラーが発生すると自動ロールバック

6. sql テンプレートタグ — 生 SQL のエスケープハッチ

import { sql } from 'kysely'

// 型安全なクエリに生 SQL を混ぜる
const result = await db
  .selectFrom('person')
  .select([
    'first_name',
    sql<number>`extract(year from created_at)`.as('created_year'),
  ])
  .where(sql`lower(first_name)`, '=', 'john')
  .execute()
// created_year の型は number として推論される

// 完全な生 SQL クエリ
const rawResult = await sql<Person>`
  SELECT * FROM person WHERE id = ${someId}
`.execute(db)

類似パッケージとの比較

特徴KyselyKnex.jsPrismaDrizzle ORM
型安全性◎ コンパイル時に完全検証△ 型定義は手動○ スキーマから生成◎ コンパイル時に検証
SQL への近さ◎ SQL をほぼそのまま書ける○ SQL ライク△ 独自 API○ SQL ライク
マイグレーション△ 別パッケージ or 自前◎ 組み込み◎ 組み込み◎ 組み込み
バンドルサイズ◎ 軽量(コアのみ)○ 中程度△ 大きい○ 軽量
学習コスト○ SQL を知っていれば低い○ 低い○ 独自概念あり○ SQL を知っていれば低い
エッジランタイム対応◎ Deno/Bun/CF Workers△ Node.js 中心△ 制限あり○ 対応
生 SQL の扱いsql タグで型付きraw メソッド$queryRawsql タグ
設計思想クエリビルダークエリビルダーORMORM + クエリビルダー

選定の目安:

  • SQL を直接書く感覚で型安全にしたい → Kysely
  • マイグレーションやリレーション管理も含めた統合環境が欲しい → Prisma / Drizzle
  • 既存の Knex プロジェクトに型安全性を加えたい → Kysely(Knex にインスパイアされた API)

注意点・Tips

スキーマ型の自動生成を活用する

手動でスキーマ型を書くのは面倒でミスの元です。kysely-codegen を使えば、既存の DB スキーマから型定義を自動生成できます。

npm install -D kysely-codegen
npx kysely-codegen --dialect postgres

executeTakeFirstexecuteTakeFirstOrThrow の使い分け

// 結果が 0 件の可能性がある場合 → undefined を含む型が返る
const maybePerson = await db
  .selectFrom('person')
  .selectAll()
  .where('id', '=', 999)
  .executeTakeFirst()
// 型: Person | undefined

// 必ず 1 件あるはずの場合 → 0 件なら例外をスロー
const person = await db
  .selectFrom('person')
  .selectAll()
  .where('id', '=', 1)
  .executeTakeFirstOrThrow()
// 型: Person

動的なクエリ構築

条件に応じてクエリを組み立てたい場合は $if を使います。

function findPersons(criteria: {
  firstName?: string
  lastName?: string
}) {
  let query = db.selectFrom('person').selectAll()

  if (criteria.firstName) {
    query = query.where('first_name', '=', criteria.firstName)
  }

  if (criteria.lastName) {
    query = query.where('last_name', '=', criteria.lastName)
  }

  return query.execute()
}

マイグレーション

Kysely にはシンプルなマイグレーション機能が組み込まれています。

import { Kysely, Migration, MigrationProvider } from 'kysely'

const migration: Migration = {
  async up(db: Kysely<any>) {
    await db.schema
      .createTable('person')
      .addColumn('id', 'serial', (col) => col.primaryKey())
      .addColumn('first_name', 'varchar(255)', (col) => col.notNull())
      .addColumn('last_name', 'varchar(255)')
      .addColumn('created_at', 'timestamp', (col) =>
        col.defaultTo(sql`now()`).notNull()
      )
      .execute()
  },
  async down(db: Kysely<any>) {
    await db.schema.dropTable('person').execute()
  },
}

destroy を忘れない

アプリケーション終了時にコネクションプールを適切に破棄してください。

await db.destroy()

ハマりやすいポイント

  • Generated 型を忘れると INSERT 時にエラーになる — auto increment や default 値を持つカラムには必ず Generated<T> を使いましょう
  • ColumnType の3つの型引数の順番ColumnType<SelectType, InsertType, UpdateType> です。混同しやすいので注意
  • PostgreSQL の returning は MySQL では使えない — DB ごとの SQL 方言の違いに注意。Kysely は方言に応じて型レベルでメソッドの有無を制御します

まとめ

Kysely は「SQL を書ける人がそのまま型安全に書ける」という絶妙なポジションのク

比較記事