はじめに
ORMを使うと生SQLの細かい制御が失われ、生SQLを書けばDBが変わるたびに書き直しが発生する。この板挟みに心当たりがある方は多いのではないでしょうか。
Knex.jsは、この中間にちょうど収まるSQLクエリビルダーです。メソッドチェーンでクエリを組み立てながら、必要なら生SQLも混ぜられる柔軟さを持ち、PostgreSQL・MySQL・SQLite3・MSSQL・CockroachDBといった複数のデータベースを同じコードで扱えます。マイグレーションやシード機能も内蔵しているため、DBスキーマのバージョン管理までひとつのツールで完結します。
Knex.jsとは
Knex.jsは「batteries-included」を謳うSQLクエリ・スキーマビルダーです。Node.js上で動作し、.select()や.where()といったメソッドチェーンでSQLを組み立てます。Objection.jsなど、Knexを土台にしたORMも存在し、クエリビルダーそのものとしても、ORMの基盤としても使われています。
主な特徴
- 複数DB対応 - PostgreSQL、MySQL、SQLite3、MSSQL、CockroachDBを共通のAPIで操作できる
- Promise/async-awaitベース - クエリの実行結果をPromiseで受け取れるため、非同期処理と自然に組み合わせられる
- マイグレーション・シード機能 - スキーマ変更履歴の管理とテストデータ投入をCLIで扱える
- 生SQLとの併用 -
knex.raw()でメソッドチェーンでは表現しにくいクエリを差し込める - コネクションプーリング - 内部で
tarn.jsを使い、接続数を制御しながら再利用する
インストール
Knex本体に加えて、使用するデータベースのドライバも合わせてインストールします。
npm install knex
# 使用するDBに応じてドライバを追加
npm install pg # PostgreSQL
npm install mysql2 # MySQL
npm install sqlite3 # SQLite3
基本的な使い方
まずは接続設定を作り、クエリビルダーでSELECT文を組み立ててみます。
const knex = require('knex')({
client: 'sqlite3',
connection: {
filename: './dev.sqlite3',
},
useNullAsDefault: true,
})
// SELECT * FROM users WHERE age >= 20 ORDER BY id
async function fetchAdults() {
const users = await knex('users')
.select('id', 'name', 'age')
.where('age', '>=', 20)
.orderBy('id')
console.log(users)
}
fetchAdults()
knex('users')でテーブルを指定し、.select()や.where()をチェーンしていくと、そのままSQLの文法に近い形でクエリを組み立てられます。INSERT・UPDATE・DELETEも同様のスタイルで書けます。
// INSERT
await knex('users').insert({ name: 'Taro', age: 25 })
// UPDATE
await knex('users').where({ id: 1 }).update({ age: 26 })
// DELETE
await knex('users').where({ id: 1 }).del()
実践的なユースケース
トランザクション処理
複数のクエリを1つの単位として扱い、途中でエラーが起きたら全体をロールバックしたい場面は少なくありません。Knexのknex.transaction()を使うと、コールバック内のクエリがすべて成功した場合のみコミットされます。
async function transferPoints(fromId, toId, amount) {
await knex.transaction(async (trx) => {
await trx('accounts')
.where({ id: fromId })
.decrement('points', amount)
await trx('accounts')
.where({ id: toId })
.increment('points', amount)
// ここで例外が投げられれば両方のクエリがロールバックされる
})
}
trxはトランザクション専用のクエリビルダーで、コールバック内で発生した例外を検知すると自動的にロールバックします。送金や在庫更新のように「全部成功するか、全部失敗するか」が求められる処理に向いています。
マイグレーションによるスキーマ管理
DBのテーブル構造をコードとして管理し、チーム全員が同じスキーマを再現できるようにするのがマイグレーションです。KnexのCLIでマイグレーションファイルを生成し、upとdownにスキーマ変更を書きます。
// migrations/20260817000000_create_posts_table.js
exports.up = function (knex) {
return knex.schema.createTable('posts', (table) => {
table.increments('id').primary()
table.string('title').notNullable()
table.text('body')
table.integer('user_id').unsigned().references('users.id')
table.timestamps(true, true)
})
}
exports.down = function (knex) {
return knex.schema.dropTable('posts')
}
npx knex migrate:make create_posts_table
npx knex migrate:latest
npx knex migrate:rollback
table.increments()やtable.references()といったスキーマビルダーのメソッドで、外部キーやNOT NULL制約もJavaScriptのコードとして表現できます。マイグレーション履歴はDB内の管理テーブルで自動的に追跡されるため、migrate:latestを実行するだけで未適用の変更だけが反映されます。
生SQLの差し込みとクエリの合成
集計関数やDB固有の構文など、メソッドチェーンだけでは書きにくいクエリも出てきます。そうした場合はknex.raw()を部分的に混ぜることで、クエリビルダーの利便性を保ったまま対応できます。
async function monthlySales() {
const result = await knex('orders')
.select(
knex.raw('DATE_TRUNC(\'month\', created_at) AS month'),
knex.raw('SUM(amount) AS total')
)
.groupByRaw('DATE_TRUNC(\'month\', created_at)')
.orderBy('month')
return result
}
.select()の引数にknex.raw()を渡すと、その部分だけ生SQLとして評価されます。プレースホルダを使えば値のバインディングも安全に行えるため、SQLインジェクションのリスクを避けながら複雑な集計クエリを組み立てられます。
まとめ
Knex.jsは、生SQLの表現力とORMの書きやすさの間で揺れがちなSQL操作を、メソッドチェーンという共通言語でまとめてくれるライブラリです。複数DBへの対応、マイグレーション・シードによるスキーマ管理、そして必要なときだけ生SQLに逃げられる柔軟さを兼ね備えており、小規模なスクリプトから本番のバックエンドまで幅広く使えます。まずは手元のプロジェクトで簡単なSELECTクエリから試してみてはいかがでしょうか。