Bun技术评估 - 05 SQL

主要特性

在开始之前,我们先来了解一下bun sql模块的主要特性,也可以看出来bun sql实现支持的程度,还有主要技术特点:

  • Tagged template literals to protect against SQL injection,使用“标记模板字面量”来保护SQL注入攻击

  • Transactions,事务支持

  • Named & positional parameters,命名和位置参数

  • Connection pooling, 连接池

  • Connection timeouts, 连接超时配置

  • BigInt support, 大整数支持

  • SASL Auth support (SCRAM-SHA-256), MD5, and Clear Text, 多样的认证支持

  • Returning rows as data objects, arrays of arrays, or Buffer,多种查询结果数据形式

  • Binary protocol support makes it faster,更快更高效的二进制协议

  • TLS support (and auth mode), TLS支持

  • Automatic configuration with environment variable,环境变量自动配置

其实,这个特性列表,编写的并不是很好。但它展示了bun sql实现的一些特点,特别是和传统的nodejs数据库框架如pg等不同的地方。我们会在后面的内容中展开探讨。

这里笔者想要强调一下,由于接触bun sql技术的时间并不是很长,很多概念和方法,还在理解和尝试当中,也许很多阐述和做法并不是很恰当,也可能有错误或不合适的地方,需要读者自行判断和注意。

数据库连接

传统的数据库框架,数据库的连接逻辑非常简单,就是提供连接参数,执行连接方法,就可以返回一个连接好的数据库对象,后续就可以执行相关数据操作了。bun也有类似的方式,其标准形式如下:


js

代码解读

复制代码

import { SQL } from "bun"; const db = new SQL({ // Required url: "postgres://user:pass@localhost:5432/dbname", // Optional configuration hostname: "localhost", port: 5432, database: "myapp", username: "dbuser", password: "secretpass", // Connection pool settings max: 20, // Maximum connections in pool idleTimeout: 30, // Close idle connections after 30s maxLifetime: 0, // Connection lifetime in seconds (0 = forever) connectionTimeout: 30, // Timeout when establishing new connections // SSL/TLS options tls: true, // tls: { // rejectUnauthorized: true, // requestCert: true, // ca: "path/to/ca.pem", // key: "path/to/key.pem", // cert: "path/to/cert.pem", // checkServerIdentity(hostname, cert) { // ... // }, // }, // Callbacks onconnect: client => { console.log("Connected to database"); }, onclose: client => { console.log("Connection closed"); }, });

但是,从bun的技术文档来看,它更推崇的是一种“环境变量控制”的连接方法。代码中没有明显的数据库连接过程,而是环境变量中,配置好连接相关信息后,执行查询时,系统会自动建立一个连接好的数据库操作环境来。

下面是一个示例:


js

代码解读

复制代码

// .env PGURL=postgres://user:pass@localhost:5432/dbname // index.ts import { sql } from "bun"; const start = async()=>{ let qr = await sql`select version()`; console.log("", qr); }; start();

这个示例中,查询代码直接使用的是从bun中import进来的sql作为标签模板函数,来执行一个sql模板字符串。当然,这个操作需要在异步函数中操作,并且增加promise的解析,就是async-await的调用方式。执行结果是一个结果对象,包含了查询的结果和其他信息,我们会再后面讨论。

这里的关键是,没有任何数据库连接的过程和代码! 只需要在项目的环境配置文件(.env)中,增加相关数据库的连接信息就可以了。而且更妙的是,bun会自动判断数据库连接的类型,并且调用合适的数据库连接驱动程序,来完成连接。

这里可能开发者会有一个疑问,就是这种方法,会不会造成反复连接,影响性能,或者造成过多的连接状态呢? 答案是应该不用为此担心,因为按照bun技术文档的说法,它的数据库连接使用了连接池(默认是10个),不需要在所有的查询都新建连接,而是直接使用连接池中的连接,这样既提高了查询的效率,又避免的连接超额的问题。

这个基于.env文件进行环境变量控制的方式,虽然简单方便,但主要问题是,不好控制这个连接的生命周期,比如连接失败,或者显示连接状态,或者增加一些连接的选项等等,那可能就需要一个手动的连接控制的形式了。

执行查询

bun的标准查询操作,使用一种“模板字面量”的方式,刚开始的时候,笔者其实是有点不适应的,因为它必须要把数据库对象和SQL语句写在一起,SQL不能是变量的形式,在其他地方进行定义和操作,和笔者习惯的准备SQL+准备参数+执行的工作流程不太吻合。

我们先来看一个示例:


js

代码解读

复制代码

// Optional WHERE clauses const filterAge = true; const minAge = 21; const ageFilter = sql`AND age > ${minAge}`; await sql` SELECT * FROM users WHERE active = ${true} ${filterAge ? ageFilter : sql``} `;

这里展示了bun sql建议的方式:

  • SQL语句的主体,是一个模板字符串(反引号围起来的)
  • 这里sql是一个标签模板函数,这里直接使用数据库对象就可以
  • 标签模板函数调用,是可以复用和组合的,从而构造更复杂的SQL语句
  • 变量作为参数,注入模板字符串,是一种更安全的方式(防SQL注入攻击)

当然,bun sql也支持笔者习惯的,比较传统的执行方式,如下:


js

代码解读

复制代码

// Multiple commands without parameters const result = await sql.unsafe(` SELECT ${userColumns} FROM users; SELECT ${accountColumns} FROM accounts; `); // Using parameters (only one command is allowed) const result = await sql.unsafe( "SELECT " + dangerous + " FROM users WHERE id = $1", [id], );

简单总结一下,可以使用一个unsafe方法,就可以实现传统(其实就是pg库)的带参数占位符的SQL语句+参数列表的执行方式了。如果实在是不喜欢标签模板函数的形式,也可以用这种方式,但这个unsafe函数名,确实是挺奇怪的?

结果集

笔者使用bun sql,还有一个不是很适应的地方,就是它对结果集的处理方式。就是,通常执行一个SQL查询的结果,并不是一个标准的结果集(通常就是一个转换的结果记录的JSON数组),而是一个“复合”的结果对象,要获得真正的结果,还需要转换一下。

  • 原始形式

例如,在前面“数据库连接”章节中,示例代码在连接数据库之后,执行了一个数据库版本信息的查询,查询结果的原始内容,其实是这样的:


js

代码解读

复制代码

[ { version: "PostgreSQL 17.5 (Ubuntu 17.5-1.pgdg20.04+1) on aarch64-unknown-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit", }, count: 1, command: "SELECT" ]

  • 数据和信息

但实际上,这个结构可以当作一个JSON数组来使用。所以,要获取其中的版本信息,可以使用 qr[0]?.version来获取。

  • values()

bun sql结果集,提供了一个values()方法,可以将查询结果,输出成为一个二维数组(而非一个对象数组),里面就只有


js

代码解读

复制代码

const rows = await sql`SELECT * FROM users`.values(); console.log(rows); [ ["Alice", "alice@example.com"], ["Bob", "bob@example.com"], ];

  • raw()

这个方法在values基础上更进一步,直接输出buffer的二维数组,据说这些可能是基于性能的考虑。比如如果要直接将结果输出到接口上,应当可以增强输出的性能。


js

代码解读

复制代码

const rows = await sql`SELECT * FROM users`.raw(); console.log(rows); // [[Buffer, Buffer], [Buffer, Buffer], [Buffer, Buffer]]

参数使用

基于防止SQL注入攻击的考虑,现在所有的SQL执行框架,都不建议直接使用SQL字符串拼装的方式,来生成和执行字符串。都是推荐使用带参数的SQL语句+参数列表的方式来构造。

在实际的业务应用开发中,会遇到各种各样数据操作的场景,很多情况下,并不是简单的单个或者固定数量参数的构造,这就对SQL执行架构的设计,提出了很多要求。经过一段时间的应用和体会,笔者觉得这套bun sql的实现,还是不错的,考虑到了很多实际的应用场景,也设计的比较简单、易用和优雅。

在本章节中,笔者会列举和分析一些典型的场景和示例,来说明这一点。

基本形式

基本形式非常简单,就是使用所谓的命名参数,来注入sql模板字符串,在前面的示例中,已经有所展示。所有单个和确定性的参数,都可以使用这种模式。

插入数据

插入数据这个操作很常见,但其实又很多需要注意的细节。下面这段代码,就很好的展示了这一点:


js

代码解读

复制代码

// Using object helper for cleaner syntax const userData = { name: "Alice", email: "alice@example.com" }; const [newUser] = await sql` INSERT INTO users ${sql(userData)} RETURNING * `;

  • 插入数据时,通常需要指定要插入的字段和对应的数据,通常就是列举字段名称和参数值
  • bun sql可以自动基于参数对象,来自动补足要填充的自动名称(使用sql构造方法)
  • 插入时,使用returning是一个比较好的操作,因为如果使用了自增字段,插入后就可以同时获取id的值

sql的构造方法非常灵活,前面的示例是构造单条记录的字段名称和值,但其实可以构造批量的数据,或者选择特定字段来处理:


js

代码解读

复制代码

const users = [ { name: "Alice", email: "alice@example.com" }, { name: "Bob", email: "bob@example.com" }, { name: "Charlie", email: "charlie@example.com", age: 25 }, ]; await sql`INSERT INTO users ${sql(users,"name","email")}`;

更新数据

在更新时,也可以利用sql构造,来确定更新时使用的字段和内容:


js

代码解读

复制代码

await sql`UPDATE users SET ${sql(user, "name", "email")} WHERE id = ${user.id}`; // uses all keys from the object to update the row await sql`UPDATE users SET ${sql(user)} WHERE id = ${user.id}`;

复合条件

下面的示例,是典型的where in查询模式


js

代码解读

复制代码

let schoolid = 1247; let clslist= [2023000,2023001,2023002]; qr = await sql` select * from b_student where clsid in ${sql(clslist)} and schoolid = ${schoolid} limit 100 `;

手动构建

笔者在使用的过程中发现,其实bun sql构造方法,其实也是某种helper的实现,也是有一定的使用方面的限制的,在某次应用,发现了以下抛出的错误:

"Helper are only allowed for INSERT, UPDATE and WHERE IN commands"

所以,如果确实有需求,在某种情况下,可能还是需要使用标准的带参数占位符的SQL构建结合参数数组使用的情况,如下面这个helper函数:


js

代码解读

复制代码

const sqlParam =(pdata)=>{ let pl = Object.keys(pdata[0]).length, ll = Array(pl).fill(0).map((_,i)=>i+1), // params idata = pdata.reduce((c,v,i)=>{ c.vlist += (i>0?",":"") + `(${ll.map(j => "$"+(i*pl+j)).join(",")})`; c.params.push(...Object.values(v)); return c; } ,{ vlist: "" , params:[]}); idata.keys = Object.keys(pdata[0]); return idata; } // 将对象数组转换为SQL占位和参数 let vparam = sqlParam([ {id: 1, name :"John"}, {id: 2, name :"Tom" }, ]); qr = await sql.unsafe(`with A(${vparam.keys}) as (values ${vparam.vlist}) select * from A`, vparam.params);

这个辅助工具,可以将一个标准的JSON对象数组,转换成为标准的SQL格式,例如在上面的案例中,转换结果为下面的内容,实现了完全的参数化:

  • vparam.keys: id,name
  • vparam.vlist: ($1,$2),($3,$4)
  • vparam.params: [1,"John",2,"Tom"]

使用这种模式,几乎可以处理所有的SQL语句和参数的执行情况。

事务支持

这一特性,笔者在实际的应用中,使用到的机会并不多,所以这里就简单的说明一下,让读者了解有这个特性即可。bun sql使用事务也很简单,执行begin就可以开启一个事务,同时将事务作为参数,注入事务的处理方法当中。在这个方法中,可以使用事务来执行一系列的SQL语句,它们会被自动的分配到同一个事务当中,保证了一致性。


js

代码解读

复制代码

await sql.begin(async tx => { // All queries in this function run in a transaction await tx`INSERT INTO users (name) VALUES (${"Alice"})`; await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`; // Transaction automatically commits if no errors are thrown // Rolls back if any error occurs });

不支持的特性

bun毕竟是一个比较新的技术框架,有很多地方,其实还没有完善。从其技术文档中,我们可以看到一些内容,读者也可以将其作为评估和决策的依据:

  • MySQL支持,正在进行中
  • 预加载连接,还在开发
  • 原生SQLite支持,已计划,但还未开始。笔者理解现有的SQLite是基于一个外部模块实现的。
  • 字段名称转换,未完成
  • 字段类型转换,未完成

关于Postgres,还有一些尚未完成的部分,但笔者觉得对于日常Web应用的开发影响不大:

  • COPY 复制,这个功能,在需要进行大量的数据导入导出时,比较有用

  • LISTEN 订阅

  • NOTIFY 通知,和订阅一样,对于实现一个实时数据库应用,还是比较关键的

此外,在文档没有提到,但笔者其实很关心的一个特性: cursor,似乎也没有支持和实现。

性能

笔者设计了一个简单的测试,来比较了一下,几乎相同的操作,使用bun实现和原有的nodejs操作,所需要的时间。不一定非常严谨和科学,只是想建立一个初步的性能概念和印象。

bun的代码实现如下:


p1.ts

代码解读

复制代码

const start = async()=>{ console.time("DATABASE"); // insert 10000 let qr,u,l = 1000; for(let i=0;i< l;i++) { u = { name: Math.random().toString(36).slice(2,8), age: 0 | Math.random()*50 + 20 } qr = await sql`insert into users ${sql(u)} returning id`; } for(let i= 300; i < 500;i++) { qr = await sql`select id,name,age from users where id = ${i} limit 1`; } console.timeEnd("DATABASE"); }; start(); // 执行结果 bun test/p1.ts [41.55s] DATABASE

nodejs参考代码:


p2.js

代码解读

复制代码

const { Client } = require('pg'); const client = new Client({ connectionString: "..." }); client.connect() .then(() => console.log('连接成功')) .catch(err => console.error('连接失败', err)); const start = async()=>{ console.time("DATABASE"); // insert 10000 let qr,u,l = 1000; for(let i=0;i< l;i++) { u = { name: Math.random().toString(36).slice(2,8), age: 0 | Math.random()*50 + 20 } qr = await client.query(`insert into users (name,age) values ($1,$2) returning id`,[u.name,u.age]); } for(let i= 300; i < 500;i++) { qr = await client.query(`select id,name,age from users where id = $1 limit 1`,[i]); } console.timeEnd("DATABASE"); }; setTimeout(start, 2000); PS C:\Work\bundev\ntest> node p2.js 连接成功 DATABASE: 4.282s PS C:\Work\bundev\ntest> bun p2.js 连接成功 [2.61s] DATABASE

如果读者观察的比较仔细的话,可能会发现一个比较不好理解的问题。就是bun sql本身的性能是比较差的(也可能是我测试的或者执行的方法不当),而且相比而言差距是比较大的。为了排除是由于对象属性解析造成的影响,笔者还曾改成了unsafe方法进行了测试,也得到了类似的结果,说明应该确实是bun sql本身的问题。这和bun声称的基于原生代码实现和优化能够提供更好的数据库操作性能,就不能匹配了。

但有趣的是,如果使用bun来执行nodejs版本的代码,就是使用pg npm库,却可以得到比nodejs执行更好的性能。简单的结论就是,bun sql本身的性能比较差(可能是实现的问题),但bun执行的性能,确实是比较好的。如果这个情况属实的话,bun sql是不适合作为数据库性性能敏感性的应用的,或者配合选择pg npm来实现。

PG模块

bun还有一个最大的优势,就是几乎完整的兼容nodejs生态,所以,如果读者觉得bun确实不能满足需求,也可以退回到标准的 pg npm。比如现在笔者在bun sql中,没有发现有关于cursor的相关功能,如果确有需求,可能需要退后到pg npm来实现这个应用的场景。

可以通过 bun add pg 指令,将pg添加到当前项目当中,添加之后,就跟在nodejs中使用pg一样的方式来操作即可。而且前面我们也已经做了简单的测试,pg npm的性能,应当是比bun sql要好的。

SQLite

在一些情况下,数据操作比较简单,这时使用标准的SQL数据库系统如Postgres,就有点杀鸡用牛刀的感觉了。这时可以考虑使用SQLite。bun也内置了SQLite的支持,除了在本地,使用文件进行操作之外,和普通的SQL数据库并没有太大的差别。但是在bun中,并不是原生实现的SQL支持SQLite,而是通过一个内部的bun:sqlite模块来实现的。下面是一些简单的示例代码:


js

代码解读

复制代码

import { Database } from "bun:sqlite"; const SQL_TB_USER=` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER DEFAULT (0) )`, SQL_INSERT_USER=`insert into users (name,age) values (?, ?)`, SQL_QUERY_USER=`select * from users where name = ? and age = ? limit ? `; const start = async()=>{ const db = new Database("mydb.sqlite", { create: true }); // in memory: const db = new Database(); db.exec(SQL_TB_USER); db.exec(SQL_INSERT_USER,['John',30]); const users = db.query(SQL_QUERY_USER).all(["John", 30, 2]); console.log("users:", users); db.close(); }; start();

这里有一些要点:

  • 使用import可以从bun:sqlite模块引入Database类
  • 创建时指定文件名,可以连接或者创建一个sqlite文件
  • 如果不指定文件名,则会在内存中创建数据库
  • 执行exec方法,结合参数,可以执行创建表、插入、更新、删除等操作
  • 执行query方法,可以创建一个查询对象
  • 调用后续的all方法,结合参数,可以执行查询,并获得结果集
  • 调用后续的get方法,将会获取查询结果中的第一个记录
  • 作为文件数据库,应当及时关闭连接,减少文件操作冲突或错误

这里只是作为介绍,展示了bun:sqlite的最基本的使用方式。更多的信息,读者可以参考bun技术文档的相关章节。

小结

在本章节中,笔者讨论了内置于bun中对postgres支持的功能:SQL。展示和例举了如数据库连接,环境配置信息,模板字符串执行的形式、unsafe模式,参数的构造和使用,结果集,事务支持等相关的内容。并补充说明了对于pg npm和sqlite的支持和实现。

<think>好的,我现在要解决用户的问题:为什么在使用`bun create next-app`时会进入Python环境,以及如何解决这个问题。首先,我需要理解这个问题的背景和可能的原因。 用户提到使用Bun来创建Next.js应用,但命令执行时进入了Python环境。Bun是一个JavaScript运行时,通常用于替代Node.js,而Next.js是一个基于React的框架,创建应用应该不需要Python。所以问题可能出在环境配置或者Bun本身的某些依赖上。 首先,我应该回忆Bun的工作原理。Bun是用Zig编写的,主要处理JavaScript和TypeScript,理论上不依赖Python。但有时候,某些工具链或脚手架可能会间接调用系统命令,可能会触发系统默认的脚本解释器。比如,有些npm包可能在安装时执行脚本,而这些脚本可能错误地调用了Python。 接下来,我需要考虑系统环境变量的问题。用户在运行`bun create`时,可能因为系统的PATH配置问题,导致某些命令被重定向到Python。例如,如果系统中存在名为`bun`的Python脚本或其他冲突的可执行文件,可能会被错误地调用。 另外,Bun本身是否有已知的问题?我需要查看Bun的官方文档或GitHub仓库的issue,看看是否有类似的情况。比如,可能Bun在创建Next.js应用时,调用了某个依赖,而该依赖需要Python,但用户的Python环境配置不正确,或者版本不兼容。 还有可能是用户误安装了其他名为`bun`的软件包。例如,Python的PyPI仓库中有一个`bun`库,用于处理某些数据格式。如果用户通过pip安装了它,可能会导致命令行中的`bun`命令指向Python包而非Bun运行时。 解决方法方面,首先应检查Bun是否正确安装。用户可以通过`bun --version`来确认。如果版本信息正常,说明安装正确。如果输出了Python相关的错误,可能说明PATH被错误配置,或者存在同名冲突的可执行文件。 用户应该检查系统的PATH变量,确保Bun的安装路径(通常是~/.bun/bin)在Python路径之前。可以使用`which bun`命令查看实际调用的bun路径。如果指向的是Python脚本,则需要调整PATH顺序或移除冲突的安装包。 另外,检查是否存在其他名为`bun`的可执行文件。比如,使用`type bun`或`where bun`(取决于系统)来查看所有可能的匹配项。如果有多个结果,需要保留正确的Bun路径,删除或重命名其他冲突的文件。 如果问题依然存在,可能需要重新安装Bun,并确保遵循官方安装指南。有时权限问题或安装过程中的中断可能导致Bun未正确安装。 总结可能的原因:1)PATH配置问题导致错误的bun被调用;2)存在其他名为bun的Python包;3)Bun安装不完整或损坏;4)Next.js模板中的某些脚本错误调用了Python。 最后,需要验证解决方法是否有效。比如,调整PATH后再次运行命令,或卸载冲突的Python包,重新安装Bun,然后测试创建应用是否正常。</think>### 问题分析:`bun create next-app` 进入 Python 环境的原因 1. **环境变量冲突** - 若系统中存在名为 `bun` 的 Python 脚本或工具,可能导致命令被错误解析为执行 Python 脚本。例如,PyPI 中存在名为 `bun` 的库(用于处理二进制数据),若用户通过 `pip install bun` 安装过该库,可能引发冲突[^3]。 - **验证方法**:执行 `which bun` 或 `where bun`,检查返回路径是否指向 Bun 的安装目录(如 `~/.bun/bin/bun`)。若路径包含 `Python` 或 `pip` 相关目录,则存在冲突。 2. **Bun 未正确安装** - Bun 的安装可能未完成或损坏,导致系统尝试通过其他方式(如 Python)解析命令。 - **验证方法**:运行 `bun --version`,若输出错误信息或 Python 相关提示,表明 Bun 未正确安装。 3. **脚手架模板的脚本依赖** - Next.js 的模板可能包含某些脚本(如 `postinstall`)间接调用 Python,但用户环境未正确配置 Python 解释器,导致异常。 --- ### 解决方法 #### 1. 检查并修复环境变量冲突 - **步骤**: 1. 查看当前 `bun` 的实际路径: ```bash which bun # Linux/macOS where bun # Windows ``` 2. 若路径指向 Python 相关目录,卸载冲突的 Python 包: ```bash pip uninstall bun # 卸载 PyPI 的 bun 库 ``` 3. 确保 Bun 的安装路径(如 `~/.bun/bin`)在 `PATH` 中且优先级高于 Python 路径。可通过修改 shell 配置文件(如 `.bashrc`、`.zshrc`)调整顺序: ```bash export PATH="$HOME/.bun/bin:$PATH" ``` #### 2. 重新安装 Bun - **步骤**: 1. 卸载现有 Bun: ```bash rm -rf ~/.bun # Linux/macOS ``` 2. 通过官方脚本重新安装: ```bash curl -fsSL https://bun.sh/install | bash ``` 3. 重启终端并验证: ```bash bun --version # 应输出类似 "1.0.11" 的版本号 ``` #### 3. 检查 Next.js 模板配置 - 若问题仅出现在特定项目模板中,尝试更新 Bun 和 Next.js 依赖: ```bash bun upgrade # 更新 Bun bun create next-app@latest my-app # 使用最新模板 ``` --- ### 典型错误示例与修复 - **错误现象**: ```bash $ bun create next-app /usr/bin/python: No module named bun ``` - **原因**:系统误将 `bun` 命令关联到 Python 解释器。 - **修复**:按上述步骤调整 `PATH` 或重新安装 Bun---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值