likes
comments
collection
share

knex.js的基本使用及动态SQL的生成

作者站长头像
站长
· 阅读数 2

Knex 是 js 编写的 SQL 查询构建器。将 SQL 运算符封装成函数,可以链式调用。

基本使用

安装依赖包

pnpm add knex
pnpm add mysql2

编写代码

以下代码来自于 Knex 官网

  1. 创建一个数据库连接
const knex = require("knex")({
  client: "mysql2",
  connection: {
    host: "127.0.0.1",
    port: 3306,
    user: "your_database_user",
    password: "your_database_password",
    database: "myapp_test",
  },
});
  1. 调用查询构建器的函数

Knex 提供了丰富的查询函数,每个查询函数和 DBMS 的关系运算符一一对应。对于只有特定的 DBMS 才支持的运算符,Knex 官方文档中也有注明。

const query = knex.select("title", "author", "year").from("books");
  1. 返回查询结果
query.then((result) => {
  console.log(result);
});

动态 SQL 构建

动态 SQL 和静态 SQL 的区别

上面的示例代码中生成的 SQL 查询是静态的,因为代码执行逻辑是固定的,不涉及到条件判断,循环执行。

如果需要根据业务逻辑,在 if-else 或者 for 循环中生成 SQL 查询语句,需要怎么做呢?

我们先看下最简单的办法:直接在 if-else 或者 for 循环中调用 knex 查询函数。

let age = 18;
// xxx 修改age的值
if (age >= 18) {
  knex.select("xxx");
} else {
  knex.select("yyy");
}

简单的业务逻辑可以这样写,但是,如果业务逻辑越来越复杂,这样写就不好维护了。

有以下缺点:

  1. knex 的执行上下文和业务逻辑耦合严重
  2. 业务逻辑中重复的部分无法复用

我们可以做以下改进:

  1. 参考策略模式,将每步关系运算分别定义成一个函数;将每种条件下的逻辑封装到一个函数 fn 中
  2. 将函数 fn 的定义和执行分开: 定义一个数组,存放函数 fn,业务逻辑专注于把不同条件下的函数放入数组中,最后统一执行数组中的每个函数

伪代码如下:

const array = [];

// 在业务逻辑中定义函数对象,按照执行顺序添加到数组中
if (age >= 18) {
  let param = 6;
  // 修改param的值
  const fn = function () {
    return this.where("xxx", param);
  };
  array.push(fn);
}

// 遍历数组,逐个执行函数
let prev = knex;
for (const item of array) {
  prev = item();
}

我们编写一个函数来构建动态 SQL。

函数定义

输入

一个函数数组,每项为一个函数,函数没有入参;如果有变量依赖,就通过闭包的形式在函数内引用外部的变量 输出

执行完查询函数的 Knex 对象

函数体逻辑

  1. 在业务逻辑中定义函数,封装某一小部分查询逻辑,把函数放入数组中
  2. 最后统一执行数组中的每个函数

代码实现

具体的业务逻辑这里就不写了,只写往数组中 push 函数的代码

  1. 定义一个数组,存放函数

    const array = [];
    
  2. 往数组中 push 函数 有几个细节要注意: 2.1 函数执行时,knex 的函数通过 this 来调用。原因:需要在上一次调用的基础上执行本次调用 2.2 函数不能用箭头函数,因为执行时要改变 this 的指向。 2.3 函数需要有返回值,返回 this(knex 的函数返回的也是 this) 2.4 如果需要依赖变量,可以在函数中直接引用外部定义的变量或者对象,形成闭包。

    const selectFn = function () {
      return this.select("t_city.name", "t_city.abbreviation");
    };
    array.push(selectFn);
    const tableFn = function () {
      return this.from("t_city");
    };
    array.push(tableFn);
    const whereFn = function () {
      return this.where("t_city.name", "广州市");
    };
    array.push(whereFn);
    
  3. 遍历数组,执行函数

遍历数组时,由于需要在上一次调用的基础上执行本函数,所以需要改变 this 的指向。这里使用了 js function 的 call 函数

本函数的返回值,需要保存下来,供下个函数调用,所以这里用了临时变量 prev 来保存函数的返回值

let prev = knex;
for (const fn of array) {
  prev = fn.call(prev);
}
const res = await prev;

最终生成的 SQL 语句如下:

select `t_city`.`name`, `t_city`.`abbreviation` from `t_city` where `t_city`.`name` = ?

代码运行结果如下:

knex.js的基本使用及动态SQL的生成

附: 数据库表结构

CREATE TABLE `t_city` (
`city_id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`abbreviation` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`city_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

完整代码

const knex = require("knex")({
client: "mysql2",
connection: {
host: "127.0.0.1",
port: 3306,
user: "your_database_user",
password: "your_database_password",
database: "myapp_test",
},
});

async function main() {
const array = [];
const selectFn = function () {
return this.select("t_city.name", "t_city.abbreviation");
};
array.push(selectFn);
const tableFn = function () {
return this.from("t_city");
};
array.push(tableFn);
const whereFn = function () {
return this.where("t_city.name", "广州市");
};
array.push(whereFn);

let prev = knex;
for (const fn of array) {
prev = fn.call(prev);
}

const res = await prev;
console.log(prev.toSQL().sql);
console.log(res);
}

main();