Qustar: 探索高效SQL数据库查询的现代JavaScript工具

文摘   2024-09-07 13:38   浙江  

Qustar

Qustar,它不仅仅是一个工具,更像是一位贴心的老朋友,懂你的需求,解你的难题。它用那简洁的 API,让我们的代码变得优雅;用那强大的功能,让我们的操作变得高效。无论是 PostgreSQL、SQLite、MySQL 还是 MariaDB,它都能游刃有余,就像是我们手中的万能钥匙,打开一个又一个数据的宝箱。

特性

✅ 表达性强且高级的查询构建器
✅ 支持 TypeScript
✅ SQL 数据库:
  ✅ PostgreSQL
  ✅ SQLite
  ✅ MySQL
  ✅ MariaDB
  ⬜ SQL Server
  ⬜ Oracle
✅ 导航属性
✅ 无需代码生成
✅ 无惊喜,所有查询产生 100% SQL
✅ 原生 SQL
⬜ 迁移
⬜ 事务

快速开始

要开始使用 qustar 与 PostgreSQL(所有支持的数据源列表见下文 #supported-database-drivers[1]),请运行以下命令:

npm install qustar qustar-pg pg

以下是 qustar 的示例用法:

import {PgConnector} from 'qustar-pg';
import {Q} from 'qustar';

// 指定一个模式
const users = Q.table({
  name: 'users',
  schema: {
    // 插入时不需要生成
    id: Q.i32().generated(), // 32 位整数
    firstName: Q.string(), // 任意文本
    lastName: Q.string(),
    age: Q.i32().null(), // 可为空的整数
  },
});

// 组合一个查询
const query = users
  .orderByDesc(user => user.createdAt)
  // map 将被翻译成 100% SQL,就像其他所有操作一样
  .map(user => ({
    name: user.firstName.concat(' ', user.lastName),
    age: user.age,
  }))
  .limit(3);

// 连接到你的数据库
const connector = new PgConnector('postgresql://qustar:passwd@localhost:5432');

// 运行查询
console.log('users:'await query.fetch(connector));

输出:

age54name'Linus Torvalds' }
age29name'Clark Kent' }
age18name'John Smith' }

上述查询将被翻译为:

SELECT
  "s1"."age",
  concat("s1"."firstName"' '"s1"."lastName"AS "name"
FROM
  users AS "s1"
ORDER BY
  ("s1"."createdAt"DESC
LIMIT
  3

插入/更新/删除:

// 插入
await users.insert({firstName: 'New', lastName: 'User'}).execute(connector);

// 更新
await users
  .filter(user => user.id.eq(42))
  .update(user => ({age: user.age.add(1)}))
  .execute(connector);

// 删除
await users.delete(user => user.id.eq(42)).execute(connector);

支持的数据库驱动

要对数据库执行查询,你需要一个 _connector_。有许多现成的连接器封装了现有的 NodeJS 驱动:

  • PostgreSQL
    • qustar-pg[2]
  • SQLite
    • qustar-better-sqlite3[3] (推荐)
    • qustar-sqlite3[4]
  • MySQL
    • qustar-mysql2[5]
  • MariaDB
    • qustar-mysql2[6]

如果你实现了自己的连接器,请告诉我,我会将其添加到上面的列表中!

使用

任何查询都从一个表或 原生 SQL[7] 开始。我们稍后会更多地讨论原生查询,现在基本用法如下:

import {Q} from 'qustar';

const users = Q.table({
  name: 'users',
  schema: {
    id: Q.i32(),
    age: Q.i32().null(),
    // ...
  },
});

在 qustar 中,你通过调用查询方法如 .filter.map 来组合查询:

const young = users.filter(user => user.age.lt(18));
const youngIds = young.map(user => user.id);

// 或者

const ids = users.filter(user => user.age.lt(18)).map(user => user.id);

查询是不可变的,因此你可以安全地重复使用它们。

对于像 .filter.map 这样的方法,你传递一个回调,它返回一个 _表达式_。表达式表示你希望执行的条件或操作。表达式是使用 .add.eq 等方法构建的:

// 对于数组,你会这样写:users.filter(x => x.age + 1 === x.height - 5)
const a = users.filter(user => user.age.add(1).eq(user.height.sub(5)));

// 你也可以使用 Q.eq 来实现相同的效果
import {Q} from 'qustar';

const b = users.map(user => Q.eq(user.age.add(1), user.height.sub(5)));

我们不能使用原生运算符如 +===,因为 JavaScript 不支持运算符重载。你可以在 这里[8] 找到支持的表达式操作的完整列表。

现在让我们谈谈查询和表达式。

查询

.filter(条件)

const adults = users
  // 年龄 >= 18 的用户
  .filter(user => /* 任何表达式 */ user.age.gte(18));

.map(映射器)

const userIds = users.map(user => user.id);

const user = users
  // 你可以映射到一个对象
  .map(user => ({id: user.id, name: user.name}));

const userInfo = users
  // 你可以映射到嵌套对象
  .map(user => ({
    id: user.id,
    info: {
      adult: user.age.gte(18),
      nameLength: user.name.length(),
    },
  }));

.orderByDesc(选择器), .orderByAsc(选择器)

const users = users
  // 按年龄升序排序
  .orderByAsc(user => user.age)
  // 然后按名字降序排序
  .thenByDesc(user => user.name);

.drop(计数), Query.limit(计数)

const users = users
  .orderByAsc(user => user.id)
  // 跳过前十个用户
  .drop(10)
  // 然后只取五个
  .limit(5);

.slice(开始, 结束)

你也可以使用 .slice 方法来实现相同的效果:

const users = users
  // 开始 = 10, 结束 = 15
  .slice(1015);

.{inner,left,right}Join(选项)

Qustar 支持 .innerJoin, .leftJoin, .rightJoin.fullJoin

const bobPosts = posts
  .innerJoin({
    right: users,
    condition: (post, user) => post.authorId.eq(user.id),
    select: (post, author) => ({
      text: post.text,
      author: author.name,
    }),
  })
  .filter(({author}) => author.like('bob%'));

.unique()

你可以使用 .unique 方法选择不同的行:

const names = users.map(user => user.name).unique();

.groupBy(选项)

const stats = users.groupBy({
  by: user => user.age,
  select: user => ({
    age: user.age,
    count: Expr.count(1),
    averageTax: user.salary.mul(user.taxRate).mean(),
  }),
});

.union(查询)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const uniqueNames = studentNames.union(teacherNames);

.unionAll(查询)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const peopleCount = studentNames.unionAll(teacherNames).count();

.concat(查询)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

// concat 保留原始排序
const allNames = studentNames.concat(teacherNames);

.intersect(查询)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const studentAndTeacherNames = studentNames.intersect(teacherNames);

.except(查询)

const studentNames = students.map(student => student.name);
const teacherNames = teachers.map(teacher => teacher.name);

const studentOnlyNames = studentNames.except(teacherNames);

.flatMap(映射器)

const postsWithAuthor = users.flatMap(user =>
  posts
    .filter(post => post.authorId.eq(user.id))
    .map(post => ({text: post.text, author: user.name}))
);

.includes(值)

const userExists = users.map(user => user.id).includes(42);

模式

支持的列类型列表:

  • boolean: 真或假
  • i8: 8 位整数
  • i16: 16 位整数
  • i32: 32 位整数
  • i64: 64 位整数
  • f32: 32 位浮点数
  • f64: 64 位浮点数
  • string: 可变长度字符串

原生 SQL

你可以像这样使用原生 SQL:

import {Q, sql} from 'qustar';

const users = Q.rawQuery({
  sql: sql`SELECT * from users`,
  // 我们必须指定模式,这样 qustar 才知道如何组合查询
  schema: {
    id: Q.i32(),
    age: Q.i32().null(),
  },
})
  .filter(user => user.age.lte(25))
  .map(user => user.id);

你也可以在嵌套查询中使用别名,像这样:

const postIds = users.flatMap(user =>
  Q.rawQuery({
    sql: sql`
      SELECT
        id
      FROM
        posts p
      WHERE p.authorId = ${user.id}'
    })`
,
    schema: {
      id: Q.i32(),
    },
  });
);

你可以使用 Q.rawExpr 在操作的一部分中使用原生 SQL:

const halfIds = users.map(user => ({
  halfId: Q.rawExpr({sql: sql`CAST(${user.id} as REAL) / 2`, schema: Q.f32()}),
  name: user.name,
}));

上述查询将被翻译为:

SELECT
  "s1"."name",
  (CAST(("s1"."id"as REAL) / 2AS "halfId"
FROM
  users AS "s1"

许可证

MIT 许可证,见 LICENSE

结语

记住,每一次代码的编写,都是一次新的探险。Qustar 将是你的罗盘,指引你穿越 SQL 的迷宫,找到那些隐藏的数据宝藏。而你,就是那位勇敢的探险家,用智慧和勇气,将这些宝藏转化为应用程序中的黄金。

所以,当你下次面对着满屏的数据库查询,感到手足无措时,别忘了你不是一个人在战斗。Qustar 就在你身边,像一位老朋友,默默地支持你,陪你一起笑对那些复杂的查询和棘手的 bug。

最后,愿你的代码永远没有 bug,愿你的查询永远高效,愿你的项目永远成功。如果你觉得 Qustar 还不错,别忘了推荐给你的小伙伴们,毕竟,好东西要一起分享嘛!再见啦,愿你在代码的海洋中,乘风破浪,一帆风顺!

参考资料
[1]

#supported-database-drivers: #supported-database-drivers

[2]

qustar-pg: https://www.npmjs.com/package/qustar-pg

[3]

qustar-better-sqlite3: https://www.npmjs.com/package/qustar-better-sqlite3

[4]

qustar-sqlite3: https://www.npmjs.com/package/qustar-sqlite3

[5]

qustar-mysql2: https://www.npmjs.com/package/qustar-mysql2

[6]

qustar-mysql2: https://www.npmjs.com/package/qustar-mysql2

[7]

原生 SQL: #raw-sql

[8]

这里: #expressions


编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章