目前的个人数据管理中台的数据库使用的是Mysql,选择的原因在前文也说过:当时没有找到如何加密SQLite的办法。巧合的是,前段时间在纠结Nuxt单体应用结构选型的时候,找到一篇老外的文章:《Just Released: Nuxt Hub Multi-Vendor and First-Class DB》,其中提到了一个新的ORM框架——Drizzle ORM。
当时被“即插即用”给吸引住了
当然,主要的是被“现在模块和层都采用了即插即用的方式来创建新的数据库表”这句话给吸引住了,于是我快速打开它的官网浏览了起来,又发现了一个SQLite的变体——Turso Database。最关键的是,它原生支持数据库加密!
Turso支持数据库加密
我让AI整理了 SQLite 与 Turso 的一些区别,内容如下:
| 特性 |
标准 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文件夹,并在其下创建data和migrations子目录,结构如下图所属
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', 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();
const client = createClient({ url: 'file:server/database/data/app.db', 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: { 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错误
四、排查问题
因为整个安装步骤几乎是跟着Gemini做下来的,所以遇到这个问题还有点懵。把报错信息扔给它后,Gemini给了一段常规排查方向:
- 检查
drizzle.config.ts的dbCredentials.url属性和server/utils/db.ts的url属性配置是否一致。
- 检查是否已运行
npx drizzle-kit push创建了数据库文件。
- 在Windows环境下,需要手动创建
server/database/data目录,否则npx drizzle-kit push有可能不会生成数据库文件。
以上回答一点用都没有,因为当你查看Drizzle官方文档时,你会发现文档中根本没提到 encryptionKey 这个参数!
指引中只有 url 和 authToken 两个参数
然后你可能会想:是不是这个encryptionKey的属性名错了,应该改成authToken?然后库吃苦吃开始一番修改:
- 修改
server/utils/db.ts的encryptionKey属性名为authToken;
- 清空
server/database/migrations和server/database/data目录下的文件;
- 重新运行
npx drizzle-kit generate和npx drizzle-kit push;
- 重启项目,访问接口测试;
接口调用成功了
噫!这么简单就改好了?然后当你想看看加密效果,碰巧用记事本打开那个数据库文件就会发现:这TM根本没加密成功啊!单纯是因为改了个参数,加密功能不生效了,才避免了报错的😥。
打开才发现根本没加密成功
那就奇怪了,不是说Turso原生支持加密的嘛?难道是AI幻觉了瞎编出来的encryptionKey参数?其实也不是,在Turso文档和其维护的客户端示例里是存在这个参数的,但是在Drizzle里却无法正常工作。
Turso文档
Turso维护的客户端示例中也存在该参数
而在Turso文档的另一个页面,我看到了另一种使用加密数据库的方式——通过url参数的形式传入加密方式和密钥。
通过url链接参数的形式传入加密方式和密钥
但是实际测试还是会出现“URL_PARAM_NOT_SUPPORTED”报错。
提示:URL_PARAM_NOT_SUPPORTED
从这步开始,所有AI工具的回复答案都是错的,我只能开始手动处理。
五、最终方案
最后,在Drizzle的官方Git仓库,我找到一篇Issue帖子《[FEATURE]:Support for Encrypted Sqlite DB file with Studio and drizzle-kit》。根据作者给出的方法,我临时解决了这个问题。
- 在
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));
|
清空server/database/migrations和server/database/data目录下的文件;
重新执行Drizzle Kit命令(注意,此处有变化!)
1 2 3 4 5
| npx drizzle-kit generate
node server/database/migrate.ts
|
- 重启项目,访问接口测试;
接口调用成功
- 再次使用记事本打开数据库文件,会发现内容已经加密了。
文件已经加密成乱码了
OK啊,问题解决,以后搭框架的时候还是不能太相信AI,毕竟它的核心能力也受知识库更新影响,即使有外置MCP、Agents可以辅助搜索实时网页,但检索和分析的能力仍比较一般。
这次数据库文件加密成功后,我应该会把项目的数据库从Mysql迁移到Turso,以后本地开发就不用再通过VMware起Mysql了,少一个依赖服务。后期想用云数据库也可以无缝迁移到Turso Cloud,一举两得😁。