Node22.5尝鲜 - node:sqlite

文章背景

某天在微信群里看到一个大佬的截图,写nodejs从22.5开始官方支持了sqlite的驱动库。

sqlite作为一个很常用的小型嵌入式数据库,在一些简单的数据持久化(其实也可以弄复杂项目)场合是非常好用的,但是在node上,之前一直都是由社区提供的方案,比如sqlitesqlite3,其中sqlite3是驱动的绑定库,用原生编写,因此在安装的时候需要通过node-gyp进行动态的编译,属于非常折磨的过程了。

因此nodejs官方默认内置sqlite,是一个非常具有突破性质的好事情。所以我立刻创建了一个新项目,打算看看官方这次搞出来的这个大宝贝到底如何。

环境准备

  • nodejs@22.5+

理论上,如果你只是想要尝鲜,只需要准备好一个22.5版本的nodejs。不过为了让自己写的舒心一些,我还额外准备一些其他的工具。

  • typescript
  • tsx
  • @types/node
  • pnpm

typescript不必说了,就是为了让自己写的代码没那么乱,tsx是一个让你"直接运行"ts文件的一个插件包,并不是react里写的那个tsx(实际上应该是jsx),它的同类插件是ts-node。

@types/node则是nodejs的类型提示包,为了能在typescript下也能提供良好的类型提示。 pnpm其实现在被我拿来当作nodejs的版本管理工具,只需要执行以下命令:

bash 复制代码
pnpm env use --global 22.5.0

但是要注意,最好使用二进制包安装的pnpm进行这类操作。

更多pnpm的env操作可以自行查看 pnpm env cmd ,这里不做赘述。

开始写代码

新建一个src/index.ts文件,然后写入以下代码

ts 复制代码
// src/index.ts
import db from "node:sqlite";

console.log(db)

然后vscode很快给出了一个错误,真是让人神清气爽。

提示node:sqlite并不存在,这个其实是很正常的现象,因为我们目前安装的@types/node的版本如果通过package.json进行查看,可以发现版本为22.0,而这个sqlite包是在22.5中被加入,并且是实验性质的包,因此在类型提示文件中并没有提供对应的类型定义。

但是,这种简单的小错误,解决起来还是很轻松的。

编写类型定义文件

typescript支持一种通过定义模块名直接扩展模块的写法,当然,这种写法也支持让我们无中生有一种模块。

因此,我们只需要通过编写module "node:sqlite"就可以消除上面的错误,但是定义类型也不能随便来,要遵守标准,而这部分标准只需要通过查看官方文档即可:SQLite | Node.js v22.5.1 Documentation (nodejs.org)

通过参考官方文档,我们成功写出了一些简单的定义

ts 复制代码
// global.d.ts
declare module "node:sqlite" {
  type PVal = null | number | bigint | string | Uint8Array | ArrayBuffer
  type RunRes = {
    lastInsertRowid: number
    changes: number
  }

  export class StatementSync<T extends {}> {
    all<V = T>(namedParameters: Partial<T>): V[]
    all<V = T>(...anonymousParameters: PVal[]): V[]
    all<V = T>(namedParameters: Partial<T>, ...anonymousParameters: PVal[]): V[]
    all<V = T>(): V[]
    get<V = T>(namedParameters: Partial<T>): V
    get<V = T>(...anonymousParameters: PVal[]): V
    get<V = T>(namedParameters: Partial<T>, ...anonymousParameters: PVal[]): V
    get<V = T>(): V
    run<V = T>(namedParameters: Partial<T>): RunRes
    run<V = T>(...anonymousParameters: PVal[]): RunRes
    run<V = T>(namedParameters: Partial<T>, ...anonymousParameters: PVal[]): RunRes
    run<V = T>(): RunRes
    sourceSQL(): string
    expandedSQL(): string
    setAllowBareNamedParameters(enable: boolean): void
    setReadBigInts(enable: boolean): void
  }

  export class DatabaseSync {
    constructor(memory: ':memory:', options?: {
      open?: boolean
    })
    constructor(location: string, options?: {
      open?: boolean
    })
    close(): void
    exec(sql: string): void
    open(): void
    prepare<T extends {} = Record<string, any>>(sql: string): StatementSync<T>
  }
}

写完以后,要记得在tsconfig中,添加include

json 复制代码
{
  "compilerOptions": {
    "allowJs": true,
    "baseUrl": "./",
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  },
  "include": [
    "src/**/*.ts",
    "global.d.ts"
  ]
}

运行代码

我们是通过tsx进行代码运行的,一般只需要执行以下指令就可以让代码跑起来

bash 复制代码
npx tsx --tsconfig ./tsconfig.json src/index.ts

不过这种方式,每次都需要指定tsconfig的路径,非常的麻烦,并且npx是从global环境下查询tsx的,而这种方式查询到的tsx也会使用global下的typescript,会导致版本不稳定的问题,所以我们可以选择将tsx跟typescript安装到项目目录下,然后通过npm-scripts来进行运行。完成后的package.json如下:

json 复制代码
{
  "name": "node22-5-sqlite-demo",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "tsx src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.16.2"
  },
  "engines": {
    "node": "^22.5.0"
  }
}

然后我们通过pnpm去调用对应的script就可以:

bash 复制代码
pnpm start

不过很不幸,我们马上就能看到第二个非常刺激的错误:

没有发现这个内置模块,其实这个问题也很简单,跟上面说到的一个重点有关,那就是,目前这个功能还是一个实验性质的模块,因此我们需要通过nodejs的flag来开启这个功能。 nodejs的flag其实就是一个特定前缀的CLI参数,即为:--experimental-*,我们要使用sqlite,则修改如下:

json 复制代码
{
    "start":"tsx --experimental-sqlite src/index.js"
}

修改完毕后,再次运行pnpm start,可以看到如下输出,代表成功:

输出了模块的完成定义,并且下发提示这是一个实验功能,后续可能会发生一些改变,不过这不是本文章关心的内容。

模块分析

node:sqlite模块目前仅提供了两个导出,分别是两个类(class):DatabaseSyncStatementSync,从名字上分析,其实就是数据库的操作对象跟语句操作对象(SQL),并且都是同步(sync)的api。

虽然目前nodejs官方并没有完全推送这个模块,但是好的是,官方文档已经对这两个api提供了比较详细的文档(也因此上面才能写出dts),所以我们直接根据官方文档来对这两个api做一个简单的了解。

DatabaseSync

这是针对数据库对象本身提供的一个构造函数,其本体是一个原生binding对象,并且其底层也是使用的sqlite3。此部分可以直接通过查看nodejs的源码看到其实现:

我们在nodejs中实例化的其实是一个原生的C对象,实例化对象的几个api也是通过napi的形式进行bingding的,因此我们并不需要关心这些api的具体实现细节,直接来看如何使用即可。

new DatabaseSync(location[, options])

构造函数,提供了两个参数,其中第一个参数location是必须的,为sqlite的存储文件路径,可以使用绝对路径也可以是相对路径;同时支持内存读取模式,使用固定的字符串":memory:""进行访问。第二个参数options是可选的配置对象,不过目前仅有open一个选项值,代表是否在实例化对象时直接打开数据库的连接(connection),默认为true

ts 复制代码
// 通过内存创建数据库,将在node进程结束时清空
const mem_db = new DatabaseSync(':memory:') 
// 将会创建db.sqlite文件(如果不存在),操作过程中的结果将会持久化在这个文件里
const db = new DatabaseSync('./db.sqlite') 

close()

用于关闭数据库的连接,这个方法内部是对sqlite3_close_v2的包装实现。

  • 如果对一个被关闭的数据库对象进行操作,会抛出一个异常。
  • 如果对一个已经关闭的数据库对象执行这个方法,也会抛出一个异常。

open()

用于开启数据库的连接,但是只能对未被开启的数据库对象进行操作,否则会抛出异常。

exec(sql)

用来立刻执行一段SQL语句,传递一个字符串参数。这个方法并不关心返回的结果,一般用来进行数据库初始化的一些的操作。

ts 复制代码
// 创建数据表
db.exec(`CREATE TABLE IF NOT EXISTS table1 (
    key INTEGER PRIMARY KEY,
    value TEXT
) STRICT`)

prepare(sql)

用来创建一个SQL预处理对象,返回的类型为StatementSync,支持通过预处理语句实现类似字符串模板插入的功能,会对拆入SQL语句的值做特殊处理,保证一些SQL语句的安全。

ts 复制代码
const query = db.prepare(`SELECT * FROM table1 WHERE key=?`)

StatementSync

这是模块提供的SQL语句预处理对象,本身是一个构造函数,但是目前文档里并没有提供直接对构造函数进行实例化的案例,所以推荐通过DatabaseSync::prepare进行创建。

StatementSync创建的实例对象提供的是一些快捷的查询操作,还有对查询语句的配置接口。不过根据实践过程来看,目前并不需要关心所有的API,因此我只在下面介绍了部分接口的用法。

需要查看更多文档,可以点击查看:SQLite | Node.js v22.5.1 Documentation (nodejs.org)

all([namedParameters][, ...anonymousParameters]) / get([namedParameters][, ...anonymousParameters])

都是用来获取查询结果的,一般配合各类查询语句进行查询。它们都支持通过在prepare中设置占位,然后传递对应的外部参数。 其中all获取的是一个数组,而get则获得是第一个匹配结果。

run([namedParameters][, ...anonymousParameters])

get或者all用法类似,但是不同的是,不再返回查询的表对象,而是只返回操作的结果。在nodejs中,返回的是一个固定格式的对象

ts 复制代码
{
    changes:number|bigint // 受到影响的行数
    lastInsertRowid:number|bigint // 最近被插入的行id
}

写一个KV存储

既然了解完了node:sqlite的用法,那么也应该写一个简单的案例来验证一下可行性。

其实为什么本人如此关心这个包的关心,很大一部分原因就是deno跟bun。

deno在实现上直接提供了对WebStorage的兼容,这意味着在deno天生自带了KV库,而bun则是内置了bun:sqlite。但是这两个目前都还不适合用在生产上,不过确实成功让nodejs社区产生了紧迫感,这点是非常好的。

所以在第一时间,就完成了一个简单的KV库实现。不过仅作为可行性的demo,并不具备任何生产能力,请勿将此demo直接用于生产代码!!! 这里先贴上demo代码的地址:mowtwo/node22-5-sqlite-demo: 用nodejs22.5的sqlite库实现一个KV (github.com)

核心代码

本人作为一个切图仔,编码能力并不是很强,因此简单的展示一下实现

ts 复制代码
// src/storage.ts
import { DatabaseSync } from "node:sqlite";

export class NodeStorage implements Storage {
  private static _storage: NodeStorage
  private static get storage() {
    if (!this._storage) {
      NodeStorage._storage = new NodeStorage()
    }

    return NodeStorage._storage
  }

  private db: DatabaseSync
  private tableName: string

  length: number

  private constructor(location?: string, tableName?: string) {
    this.tableName = tableName ?? `__nodeStorage_${Date.now()}`
    this.db = new DatabaseSync(location ?? ':memory:')
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS ${this.tableName}(
        key TEXT PRIMARY KEY,
        value TEXT
      ) STRICT
    `)
    this.length = 0
  }

  private reLength() {
    this.length = this.db.prepare<{
      'COUNT(*)': number
    }>(`SELECT COUNT(*) FROM ${this.tableName}`).get()?.["COUNT(*)"] ?? 0
  }

  clear(): void {
    this.db.exec(`DELETE FROM ${this.tableName}`)
    this.length = 0
  }
  getItem(key: string): string | null {
    return this.db.prepare<{
      value: string
    }>(
      `SELECT value FROM ${this.tableName} WHERE key=?`
    ).get(key)?.value
  }
  key(index: number): string | null {
    return this.db.prepare<{
      value: string
    }>(
      `SELECT value FROM ${this.tableName} WHERE rowid=?`
    ).get(index + 1)?.value
  }
  removeItem(key: string): void {
    this.db.prepare(
      `DELETE FROM ${this.tableName} where key=?`
    ).run(key)
  }
  setItem(key: string, value: string): void {
    this.db.prepare(
      `INSERT OR REPLACE INTO ${this.tableName} (key, value) VALUES (?, ?)`
    ).run(key, value)
    this.reLength()
  }
  static get length() {
    return NodeStorage.storage.length
  }
  static clear() {
    return NodeStorage.storage.clear()
  }
  static getItem(key: string) {
    return NodeStorage.storage.getItem(key)
  }
  static key(index: number) {
    return NodeStorage.storage.key(index)
  }
  static removeItem(key: string) {
    return NodeStorage.storage.removeItem(key)
  }
  static setItem(key: string, value: string) {
    return NodeStorage.storage.setItem(key, value)
  }

  static __fork(location: string, tableName?: string) {
    return new NodeStorage(location, tableName)
  }
}

通过一个NodeStorage类构建主要的代码实现,并对外释放一个默认的单例存储对象,此方法创建的数据库在使用:memory:形式进行存储。

提供了静态的__fork方法可以构建一个指定存储路径的对象,并且可以指定存储的表名。 通过此方法就可以分别实现类似localStorage(本地持久化)和sessionStorage(单次会话持久化)。

使用案例

ts 复制代码
// src/index.ts
import { NodeStorage as _ } from "./libs/storage.js";

const NodeStorage = _.__fork('data.sqlite', 'wtf')

console.log(NodeStorage.getItem('name'))

NodeStorage.setItem('name', 'Mowtwo')

NodeStorage.setItem('age', '21')

console.log(NodeStorage.getItem('name'))
console.log(NodeStorage.getItem('age'))

console.log(NodeStorage.length)

NodeStorage.removeItem('age')

console.log(NodeStorage.length)

console.log(NodeStorage.key(0))

NodeStorage.setItem('age', '26')

console.log(NodeStorage.getItem('age'))

console.log(NodeStorage.length)

运行结果

文章总结

总体上仅作为对nodejs22.5.0更新的node:sqlite库做一个体验,本次更新其实还带来了很多其他的变化,不过对本人来说并没有特别大的兴趣。听闻后续nodejs也在跟进typescript的运行时支持,所以接下来就可以期待一下能否支持内置node:jsx,nodejs今年的变化是非常让人惊喜的,来自另外两个竞争对手的压力让它不再摆烂,这是一件好事。

相关推荐
好开心3315 分钟前
axios的使用
开发语言·前端·javascript·前端框架·html
Domain-zhuo24 分钟前
Git常用命令
前端·git·gitee·github·gitea·gitcode
菜根Sec1 小时前
XSS跨站脚本攻击漏洞练习
前端·xss
m0_748257181 小时前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
桃园码工1 小时前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲2 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
Anlici2 小时前
three.js建立3D模型展示地球+高亮
前端·数据可视化·canvas
轻口味2 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami2 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda3 小时前
React Native 集成原生Android功能
javascript·react native·react.js