在Nuxt4中集成Drizzle ORM + SQLite加密

目前的个人数据管理中台的数据库使用的是Mysql,选择的原因在前文也说过:当时没有找到如何加密SQLite的办法。巧合的是,前段时间在纠结Nuxt单体应用结构选型的时候,找到一篇老外的文章:《Just Released: Nuxt Hub Multi-Vendor and First-Class DB》,其中提到了一个新的ORM框架——Drizzle ORM

当时被“即插即用”给吸引住了当时被“即插即用”给吸引住了

当然,主要的是被“现在模块和层都采用了即插即用的方式来创建新的数据库表”这句话给吸引住了,于是我快速打开它的官网浏览了起来,又发现了一个SQLite的变体——Turso Database。最关键的是,它原生支持数据库加密

Turso支持数据库加密Turso支持数据库加密

我让AI整理了 SQLiteTurso 的一些区别,内容如下:

特性 标准 SQLite Turso (libSQL)
类型系统 灵活关联(Affinity)。 兼容 Affinity,但支持更严格的类型检查模式。
存储类 NULL, INTEGER, REAL, TEXT, BLOB。 同左,但对 Uint8Array (Blob) 的处理在驱动层更友好。
布尔值 实际上是 0 或 1。 依然是 0 或 1,但在 Drizzle 中可以通过 boolean映射。
扩展支持 需要手动加载 .so.dll扩展。 内置向量搜索 (Vector) ,支持 F32_BLOB等类型用于 AI 向量存储。
加密实现方式 通过 PRAGMA key = 'password' 指令 libSQL 原生支持加密 (Encryption at rest)
加密便利性 一般
加密方案优点 行业标准,加密非常彻底(包括元数据),文件离开环境后完全无法读取。 配置简单 ,只需要在连接字符串或配置对象中传入一个 encryptionKey,且未来如果使用云同步可以无缝支持。
加密方案缺点 依赖较重。在 Node.js 中需要编译二进制驱动(如 better-sqlite3-multiple-ciphers),在不同的操作系统(Windows/Linux/Mac)间部署时,有时会遇到编译环境报错。 它是 libSQL 特有的实现,如果你以后想换回最原始的 SQLite 引擎,可能需要解密导出再导入。

一、安装方式

01、安装npm包

1
2
3
4
5
# 安装运行时依赖
npm install drizzle-orm @libsql/client

# 安装开发依赖
npm install -D drizzle-kit

02、创建目录

在Nuxt的 server 目录下创建database文件夹,并在其下创建datamigrations子目录,结构如下图所属

1
2
3
4
5
.
`-- server/
`-- database/
|-- data/ // 存储数据库文件
`-- migrations/ // 存储表结构变动的数据库迁移文件

03、创建数据库 Schema

文件位置:server/database/schema.ts

1
2
3
4
5
6
7
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"

export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull(),
})

上面是一个基础的用户表结构,用于测试集成效果。当前的目录结构如下:

1
2
3
4
5
6
.
`-- server/
`-- database/
|-- data/ // 存储数据库文件
|-- migrations/ // 存储表结构变动的数据库迁移文件
`-- schema.ts // 表结构定义文件 *new

04、配置 Drizzle Kit (用于迁移)

在项目根目录创建 drizzle.config.ts

1
2
3
4
5
6
7
8
9
10
11
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
schema: './server/database/schema.ts',
out: './server/database/migrations',
dialect: 'turso', // 这里选择 turso 以兼容 libSQL
dbCredentials: {
// 这里的配置仅用于本地生成迁移文件
url: 'file:server/database/data/local.db',
},
});

当前的目录结构如下:

1
2
3
4
5
6
7
8
.
|-- server/
| `-- database/
| |-- data/ // 存储数据库文件
| |-- migrations/ // 存储表结构变动的数据库迁移文件
| `-- schema.ts // 表结构定义文件
|
`-- drizzle.config.ts // drizzle配置 *new

05、创建数据库实例类

server/utils/目录下创建db.ts实例类文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import * as schema from '../database/schema';

let _db: ReturnType<typeof drizzle<typeof schema>> | null = null;

export const useDb = () => {
if (!_db) {
const config = useRuntimeConfig();

// 创建 libSQL 客户端
const client = createClient({
url: 'file:server/database/data/app.db', // 数据库文件存放路径
// 传入加密密钥即可开启自动加解密
// 生产环境务必通过 .env 配置:NUXT_DB_ENCRYPTION_KEY
encryptionKey: config.dbEncryptionKey || 'your-fallback-secret-key',
});

_db = drizzle(client, { schema });
}
return _db;
};

同时,在 nuxt.config.ts 中配置运行时的环境变量:

1
2
3
4
5
6
export default defineNuxtConfig({
runtimeConfig: {
// 记得把NUXT_DB_ENCRYPTION_KEY配置到 .env 文件哦
dbEncryptionKey: process.env.NUXT_DB_ENCRYPTION_KEY,
}
})

当前的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
.
|-- server/
| |-- database/
| | |-- data/ // 存储数据库文件
| | |-- migrations/ // 存储表结构变动的数据库迁移文件
| | `-- schema.ts // 表结构定义文件
| |
| `-- utils/
| `-- db.ts // 数据库实例类 *new
|
|-- .env // 环境变量 *new
|-- drizzle.config.ts // drizzle配置
`-- nuxt.config.ts // nuxt配置 *new

到这一切都很顺利

二、运行命令

1
2
3
4
5
# 生成迁移文件(只写文件,不改数据库)
npx drizzle-kit generate

# 直接同步数据库(直接改库,不生成文件)
npx drizzle-kit push

运行命令后,Drizzle 会生成数据库文件和迁移文件。当前的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
|-- server/
| |-- database/
| | |-- data/ // 存储数据库文件
| | | `-- local.db // 数据库文件 *new
| | |
| | |-- migrations/ // 存储表结构变动的数据库迁移文件
| | | |-- meta/
| | | `-- xxx.sql // 生成的迁移文件 *new
| | |
| | `-- schema.ts // 表结构定义文件
| |
| `-- utils/
| `-- db.ts // 数据库实例类
|
|-- .env // 环境变量
|-- drizzle.config.ts // drizzle配置
`-- nuxt.config.ts // nuxt配置

三、测试数据库

让我们在server/api/目录下创建1个写入接口(我这里命名为 server/api/user-add.get.ts),用于测试数据库是否已正常工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useDb } from '#server/utils/db';
import { users } from '#server/database/schema';

export default defineEventHandler(async (event) => {
const db = useDb();

const newUser = await db.insert(users).values({
name: 'user' + (Math.random() * 100000).toFixed(),
email: `${(Math.random() * 10000000).toFixed()}@test.com`,
}).returning();

return newUser;
});

当前的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
|-- server/
| |-- api/
| | `-- user-add.get.ts // 接口文件 *new
| |
| |-- database/
| | |-- data/ // 存储数据库文件
| | | `-- local.db // 数据库文件
| | |
| | |-- migrations/ // 存储表结构变动的数据库迁移文件
| | | |-- meta/
| | | `-- xxx.sql // 生成的迁移文件
| | |
| | `-- schema.ts // 表结构定义文件
| |
| `-- utils/
| `-- db.ts // 数据库实例类
|
|-- .env // 环境变量
|-- drizzle.config.ts // drizzle配置
`-- nuxt.config.ts // nuxt配置

接着运行项目,打开浏览器访问http://(Nuxt本地域名)/api/user-add,然后你大概率会看到以下界面:

在CLI控制台,你会看到SQLITE_NOTADB: file is not a database错误在CLI控制台,你会看到SQLITE_NOTADB: file is not a database错误

四、排查问题

因为整个安装步骤几乎是跟着Gemini做下来的,所以遇到这个问题还有点懵。把报错信息扔给它后,Gemini给了一段常规排查方向:

  1. 检查drizzle.config.tsdbCredentials.url属性和server/utils/db.tsurl属性配置是否一致。
  2. 检查是否已运行npx drizzle-kit push创建了数据库文件。
  3. 在Windows环境下,需要手动创建server/database/data目录,否则npx drizzle-kit push有可能不会生成数据库文件。

以上回答一点用都没有,因为当你查看Drizzle官方文档时,你会发现文档中根本没提到 encryptionKey 这个参数

指引中只有 url 和 authToken 两个参数指引中只有 url 和 authToken 两个参数

然后你可能会想:是不是这个encryptionKey的属性名错了,应该改成authToken?然后库吃苦吃开始一番修改:

  1. 修改server/utils/db.tsencryptionKey属性名为authToken
  2. 清空server/database/migrationsserver/database/data目录下的文件;
  3. 重新运行npx drizzle-kit generatenpx drizzle-kit push
  4. 重启项目,访问接口测试;

接口调用成功了接口调用成功了

噫!这么简单就改好了?然后当你想看看加密效果,碰巧用记事本打开那个数据库文件就会发现:这TM根本没加密成功啊!单纯是因为改了个参数,加密功能不生效了,才避免了报错的😥。

打开才发现根本没加密成功打开才发现根本没加密成功

那就奇怪了,不是说Turso原生支持加密的嘛?难道是AI幻觉了瞎编出来的encryptionKey参数?其实也不是,在Turso文档和其维护的客户端示例里是存在这个参数的,但是在Drizzle里却无法正常工作

Turso文档Turso文档

Turso维护的客户端示例中也存在该参数Turso维护的客户端示例中也存在该参数

而在Turso文档的另一个页面,我看到了另一种使用加密数据库的方式——通过url参数的形式传入加密方式和密钥

通过url链接参数的形式传入加密方式和密钥通过url链接参数的形式传入加密方式和密钥

但是实际测试还是会出现“URL_PARAM_NOT_SUPPORTED”报错。

提示:URL_PARAM_NOT_SUPPORTED提示:URL_PARAM_NOT_SUPPORTED

从这步开始,所有AI工具的回复答案都是错的,我只能开始手动处理。

五、最终方案

最后,在Drizzle的官方Git仓库,我找到一篇Issue帖子《[FEATURE]:Support for Encrypted Sqlite DB file with Studio and drizzle-kit》。根据作者给出的方法,我临时解决了这个问题。

  1. server/database/目录下创建migrate.ts,手动处理数据表迁移过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema.js';
import { sql } from 'drizzle-orm';
import 'dotenv/config';

if (!process.env.DB_FILE_NAME) {
throw new Error('DB_FILE_NAME is not set');
}

if (!process.env.ENCRYPTION_KEY) {
throw new Error('ENCRYPTION_KEY is not set');
}

export const client = createClient({
url: `file:${process.env.DB_FILE_NAME}`,
encryptionKey: process.env.ENCRYPTION_KEY,
});

export const db = drizzle(client, { schema });

async function createTables() {
const { migrate } = await import('drizzle-orm/libsql/migrator');
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Tables created successfully');
}

async function checkTables() {
const tables = await db.select({ name: sql<string>`name` })
.from(sql`sqlite_master`)
.where(sql`type = 'table'`)
.all();

console.log('Tables in the database:', tables.map(t => t.name));
return tables.map(t => t.name);
}

createTables()
.then(checkTables)
.then((tables) => {
console.log('All tables created successfully:', tables);
})
.catch((error) => console.error('Error creating or checking tables:', error));
  1. 清空server/database/migrationsserver/database/data目录下的文件;

  2. 重新执行Drizzle Kit命令(注意,此处有变化!

1
2
3
4
5
# 生成迁移文件(只写文件,不改数据库)
npx drizzle-kit generate

# 使用自定义迁移过程
node server/database/migrate.ts
  1. 重启项目,访问接口测试;

接口调用成功接口调用成功

  1. 再次使用记事本打开数据库文件,会发现内容已经加密了。

文件已经加密成乱码了文件已经加密成乱码了

OK啊,问题解决,以后搭框架的时候还是不能太相信AI,毕竟它的核心能力也受知识库更新影响,即使有外置MCP、Agents可以辅助搜索实时网页,但检索和分析的能力仍比较一般。

这次数据库文件加密成功后,我应该会把项目的数据库从Mysql迁移到Turso,以后本地开发就不用再通过VMware起Mysql了,少一个依赖服务。后期想用云数据库也可以无缝迁移到Turso Cloud,一举两得😁。

在Nuxt4中集成Drizzle ORM + SQLite加密

作者:有点东西

链接: https://www.youdiandongxi.com/article/nuxt-with-drizzle-and-turso.html

协议:本文采用 CC BY-NC-SA 4.0 隐私协议,转载请注明出处!

评论区