Knex.js 命令速查表 - Knex SQL 查询构建器常用命令大全
面向想用 JavaScript 写 SQL、并能跨数据库迁移的 Node.js 开发者。Knex 的价值是可链式调用、自动参数绑定、不绑死某一种数据库;但要记得只有 .then/await 才会真正执行,以及何时用 raw() 逃生舱比继续构建表达式更清晰。读完能确定性地写出 CRUD、Join 与 groupBy 链,把多语句逻辑放进带回滚的事务,并用 debug/toString 查看生成的原生 SQL。
典型使用场景
面向想用 JavaScript 写 SQL、并且希望一套代码能跨 PostgreSQL / MySQL / SQLite 的开发者。Knex 的价值在于链式调用、自动参数绑定与跨库迁移能力;代价是各方言能力有差异(returning 只在 PG/SQLite 可用)。本表覆盖查询、写入、Join、聚合、事务、Schema 构建与原始查询七组,事务与防注入两节建议连着看。
查询数据 10
knex("users").select("*")knex("users").select("name", "email")knex("users").where("age", ">", 18)knex("users").where({ status: "active" })knex("users").whereNull("deleted_at")knex("users").orderBy("created_at", "desc")knex("users").limit(10).offset(20)knex("users").first()knex("users").pluck("id")knex("users").distinct("country")插入数据 5
knex("users").insert({ name: "John", email: "john@example.com" })knex("users").insert([{ name: "A" }, { name: "B" }])knex("users").insert(data).returning("id")knex("users").insert(data).onConflict("email").merge()knex("users").insert(data).onConflict("email").ignore()更新与删除 5
knex("users").where("id", 1).update({ name: "Jane" })knex("users").where("id", 1).update({ views: knex.raw("views + 1") })knex("users").where("id", 1).update({ deleted_at: knex.fn.now() })knex("users").where("id", 1).del()knex("users").truncate()Join 关联 6
knex("users").join("posts", "users.id", "posts.user_id")knex("users").leftJoin("posts", "users.id", "posts.user_id")knex("users").rightJoin("posts", "users.id", "posts.user_id")knex("users").fullOuterJoin("posts", "users.id", "posts.user_id")knex("users").crossJoin("posts")knex("users").join("posts", function() { this.on("users.id", "=", "posts.user_id").andOn("users.active", "=", knex.raw("?", [1])) })聚合与分组 8
knex("users").count("id as total")knex("users").count("* as total").first()knex("orders").sum("amount")knex("users").max("age")knex("users").min("age")knex("orders").avg("amount")knex("users").groupBy("status")knex("users").groupBy("status").having(knex.raw("count(*) > ?", [5]))事务 4
knex.transaction(async (trx) => { await trx("users").insert(data) })const trx = await knex.transaction(); await trx.commit() / trx.rollback()const sp = await trx.savepoint(async (sp) => { /* ... */ })await knex.transaction(cb, { isolationLevel: "read committed" })Schema 构建 7
knex.schema.createTable("users", (t) => { t.increments("id"); t.string("name") })knex.schema.dropTableIfExists("users")knex.schema.alterTable("users", (t) => { t.string("email") })t.integer("age").unsigned().notNullable().defaultTo(0)t.timestamps(true, true)knex.schema.table("users", (t) => { t.index(["name", "status"]) })knex.schema.table("users", (t) => { t.unique("email") })原始查询与调试 5
knex.raw("SELECT * FROM users WHERE id = ?", [1])knex("users").where("active", true).debug(true)knex("users").where("id", 1).toSQL().toNative()knex("users").where("id", 1).toString()knex("users").columnInfo()参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
.where(col, op, val) | 条件查询,三参形式可写比较符 | knex('users').where('age', '>', 18) |
.whereNull(col) | 生成 IS NULL | knex('users').whereNull('deleted_at') |
.onConflict(col).merge() | upsert:冲突时更新已有行 | .onConflict('email').merge() |
.returning(id) | 插入后返回主键(仅 PG / SQLite) | .insert(data).returning("id") |
.del() | 删除匹配行 | knex('users').where('id', 1).del() |
.truncate() | 清空整表并重置自增(不可回滚) | knex('users').truncate() |
knex.raw(sql, bindings) | 原生 SQL,? 为参数占位符 | knex.raw("SELECT * FROM users WHERE id = ?", [1]) |
.transaction(async (trx) => {}) | 自动提交/回滚的事务包装 | await knex.transaction(async (trx) => { ... }) |
trx.savepoint(cb) | 保存点:内层失败只回滚该段 | const sp = await trx.savepoint(async (s) => { ... }) |
.having(knex.raw(...)) | 分组后按聚合值过滤 | .having(knex.raw("count(*) > ?", [5])) |
.debug(true) | 把生成的 SQL 打到控制台 | knex('users').debug(true) |
.toSQL().toNative() | 查看 SQL 与绑定值,不执行 | knex("users").where("id",1).toSQL().toNative() |
易错点与避坑指南
现象把用户输入拼进 knex.raw 后站点出现 SQL 注入风险。
原因字符串拼接绕过了 Knex 的参数绑定机制。
处置改用 knex.raw("... WHERE id = ?", [id]),或直接用链式 API。
现象事务执行了一部分后报错,之前的插入也没了。
原因事务回调内的查询忘记 await,事务在操作完成前就提交了。
处置确保回调内每个查询都 await;否则部分操作会跑在事务之外。
现象误用 truncate 清空了生产表且无法回滚。
原因truncate 是不可回滚的 DDL,且通常不接受 where。
处置生产环境改用 .where(...).del() 并先在小批量上验证。
现象upsert 后原有记录的其他字段被清空了。
原因.merge() 不指定列时会覆盖整行。
处置.merge(['name', 'updated_at']) 显式列出要更新的列。
现象同一段查询在 PostgreSQL 正常、在 MySQL 报错找不到 returning。
原因returning 是 PG / SQLite 特性,MySQL 不支持。
处置按方言分支处理,或改用 insert 后单独查询。
现象长连接闲置后首次查询报连接错误。
原因knex 实例被跨进程共享,连接已在别处失效。
处置每个进程各自初始化 knex 实例,不共享连接池对象。
排障路径
1先看生成的 SQL 对不对
knex("users").where("id", 1).toSQL().toNative()不执行只打印 SQL 与绑定值,排查方言差异最快。
2确认字段名与表结构
await knex.schema("users").columnInfo()字段拼错时 SQL 会在运行时报数据库错误,先核对结构。
3排查依赖版本冲突
npm ls knex --depth=0确认装的是哪一版 knex 与哪一版数据库驱动。
4事务失败时定位是哪一步
await knex.transaction(async (trx) => { await trx("a").insert(x); await trx("b").insert(y) })逐步注释找出失败的那条;配合保存点可只回滚其中一段。
5查看整棵依赖树
npm why knex确认是否被某个上层依赖带入,避免多版本共存。
命令示例
用 ? 占位符防止 SQL 注入
knex('users').where('name', '?', name).toString()
// 反例:knex.raw('SELECT * FROM users WHERE name = ' + name)链式 API 自动绑定参数。确需原生 SQL 时用 knex.raw("... WHERE id = ?", [1]),绝不要把变量直接拼进语句。
输出
select * from `users` where `name` = 'tom'
调试:打印实际生成的 SQL
knex('users').where('active', true).toSQL().toNative()需要看控制台输出可加 .debug(true)。生成结果与预期不符时,先看 SQL 再看数据,往往能立刻定位是方言差异还是逻辑问题。
输出
{
sql: 'select * from `users` where `active` = ?',
bindings: [ true ]
}upsert:冲突时更新或忽略
await knex('users')
.insert({ id: 1, name: 'ali' })
.onConflict('id')
.merge()onConflict("列名") 指定冲突键,后跟 .merge() 更新已有记录或 .ignore() 跳过。注意 .merge() 不加列表会覆盖整行,重要字段建议显式列出。
事务与保存点
await knex.transaction(async (trx) => {
await trx('users').insert({ id: 1 })
const sp = await trx.savepoint(async (s) => {
await s('orders').insert({ id: 1 })
})
})保存点让内层失败只回滚该段而保留外层成果;没有它,一次失败会撤销整个事务的改动。回滚时抛出的错误即触发点。
常见坑与注意事项
- knex.raw 拼接字符串等同于放开 SQL 注入,必须用 ? 占位符加 bindings 数组。
- truncate 会清空整表并重置自增,是不可回滚的 DDL 且通常不带 where,不要在生产表上顺手执行。
- .merge() 不指定列时会覆盖整行,可能清空未列出的字段;关键字段显式列出。
- 事务回调内必须 await 所有查询;忘记 await 会导致事务在操作完成前就提交。
- 同一连接池内不可跨进程共享 knex 实例,连接会失效;每进程各自初始化。
提示
- Knex 支持 PostgreSQL、MySQL、MariaDB、SQLite、MSSQL,连接时指定 client。
- 查询构建器返回 Promise,需要 await 或 .then() 才会执行。
- knex.raw("SQL", bindings) 执行原生 SQL,参数绑定可防 SQL 注入。
- Migration 用 knex.migrate.make() 创建,knex.migrate.latest() 执行迁移。
- 连接池默认启用,配置 pool: { min: 2, max: 10 } 调整大小。
常见问题
Knex 怎么防止 SQL 注入?
不要自己拼接 SQL 字符串,用链式 API,Knex 会自动做参数绑定:knex("users").where("id", 1) 而不是 knex.raw("SELECT * FROM users WHERE id = " + id)。确实需要原生 SQL 时走 knex.raw("... WHERE id = ?", [1]),用 ? 占位符并把值放进数组,切勿把变量直接插进语句。链式调用上还可以加 .debug(true) 打印实际生成的 SQL 来复核。
truncate 和 del 有什么区别?
del() 只删除符合条件的行,表结构和自增序列保持不变;truncate() 清空整张表并重置自增主键,在多数数据库里底层是更高效的批量删除,但它是不可回滚的 DDL 操作且通常不能带 where 条件。清理测试数据常用 truncate,但它会一并删掉全部数据,别在生产库上顺手执行。
怎么调试一条 Knex 查询?
链上加 .debug(true) 会把生成的 SQL 打印到控制台,最直接。也可以用 .toSQL() 拿到 SQL 对象,.toSQL().toNative() 还能看到具体的驱动实现与绑定值,排查方言差异时很有用。columnInfo() 可以获取表结构,确认字段名是否写对。
保存点(savepoint)能解决什么问题?
事务里如果只有部分操作失败,需要回滚那一段而保留其余成果时用它。trx.savepoint(async (sp) => { ... }) 在其中失败只回滚到保存点,外层事务的其他修改仍然有效。没有嵌套保存点时,一次失败会把整个事务的改动全部撤销,这往往不是你想要的。
Knex 怎么支持多种数据库?
通过 knex 初始化时指定 client,比如 knex({ client: "pg" }) 或 "mysql2"、"sqlite3",同一套查询代码就能在 PostgreSQL、MySQL、SQLite 上跑。代价是各方言的能力有差异,比如 returning 只在 PostgreSQL 和 SQLite 可用,fullOuterJoin 也不是所有数据库都支持,写跨库代码时要留意。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。