修复 SvelteKit + Drizzle + MySQL 'promise' 未定义错误
2025-04-03 08:14:22
修复 SvelteKit + Drizzle + MySQL 中的 'Cannot read properties of undefined (reading 'promise')' 错误
搞 SvelteKit 项目,配上 MySQL 数据库和 Drizzle ORM,用 TypeScript 写,听起来挺顺手的吧?但有时候,初始化 Drizzle 的时候,冷不丁就给你报个错:
TypeError: Cannot read properties of undefined (reading 'promise')
at isCallbackClient (file:///home/user/x/node_modules/drizzle-orm/mysql2/driver.js:62:24)
at construct (file:///home/user/x/node_modules/drizzle-orm/mysql2/driver.js:36:29)
at drizzle (file:///home/user/x/node_modules/drizzle-orm/mysql2/driver.js:82:10)
at /home/user/x/src/lib/db/index.ts:4:19
... (省略其他调用栈信息)
这错误信息看着有点懵,尤其是在你感觉代码挺简单直接的情况下。比如,你的 src/lib/db/index.ts
文件可能长这样:
// src/lib/db/index.ts
import { drizzle } from 'drizzle-orm/mysql2';
import { DB_URL } from '$env/dynamic/private'; // 从环境变量获取数据库连接 URL
// 问题就出在这一行!
export const db = drizzle(DB_URL);
然后在你的 +page.server.ts
或者其他服务端代码里,你尝试导入并使用它:
// src/routes/+page.server.ts (或其他地方)
import { db } from '$lib/db';
// ... 在 load 函数或其他地方尝试使用 db ...
结果就触发了上面的 TypeError
。这到底是咋回事呢?
刨根问底:为什么会出错?
这个问题的核心在于 Drizzle ORM 的 mysql2
驱动程序期望接收的参数类型。
看错误堆栈,出错点在 drizzle-orm/mysql2/driver.js
这个文件里。它在内部尝试访问一个对象的 promise
属性,但那个对象是 undefined
,所以就报错了。
翻看 Drizzle ORM 关于 mysql2
驱动的文档或者源码会发现,drizzle()
函数并不直接接受一个数据库连接 URL 字符串。它需要的是一个已经实例化的 mysql2
连接对象 或者连接池对象 。
你提供的代码:
export const db = drizzle(DB_URL);
这里,DB_URL
只是一个包含数据库连接信息的字符串,比如 "mysql://user:password@host:port/database"
。Drizzle 拿到这个字符串,它不知道怎么直接用它来连接数据库并执行操作,它期望的是一个能调用 .promise()
方法或者符合特定接口的 mysql2
客户端实例。你传了个字符串,它自然找不到需要的 promise
属性,就报错了。
简单来说:你喂给 Drizzle 的不是它想吃的“饭”(连接对象/池),而是一张“菜单”(连接字符串)。
对症下药:解决方案
明白了原因,解决起来就思路清晰了:我们需要在调用 drizzle()
之前,先用 DB_URL
创建一个 mysql2
的连接实例或连接池实例,然后把这个实例传给 drizzle()
。
推荐使用连接池 (Pool
),因为它能更好地管理数据库连接,提高应用性能和稳定性,特别是在 Web 服务这种并发请求场景下。
方案一:使用 mysql2/promise
创建连接池 (推荐)
这是最常见也是推荐的方式。
-
安装
mysql2
驱动:
如果你还没装,先把它装上:npm install mysql2 # 或者 yarn add mysql2 # 或者 pnpm add mysql2
-
修改
src/lib/db/index.ts
:// src/lib/db/index.ts import { drizzle } from 'drizzle-orm/mysql2'; import mysql from 'mysql2/promise'; // 导入 mysql2 的 promise 版本 import { DB_URL } from '$env/dynamic/private'; // 保持从环境变量获取 // 检查 DB_URL 是否存在,增加健壮性 if (!DB_URL) { throw new Error("数据库连接字符串 'DB_URL' 未在环境变量中设置。"); } // 1. 创建一个 mysql2 连接池 // createPool 比 createConnection 更好,因为它管理多个连接,适合 Web 应用 const pool = mysql.createPool({ uri: DB_URL, // 直接使用连接 URI // 你也可以在这里添加其他 mysql2 连接池配置,例如: // connectionLimit: 10, // 最大连接数 // waitForConnections: true, // 连接达到上限时是否等待 // queueLimit: 0 // 等待队列限制,0表示不限制 }); // 2. 将连接池实例传递给 drizzle export const db = drizzle(pool); // (可选) 导出 pool 本身,可能在某些特殊情况下需要直接操作连接池 // export const connectionPool = pool;
- 原理和作用:
- 我们引入了
mysql2/promise
,这是mysql2
库提供的支持 Promise API 的版本,更适合现代的 async/await 语法。 mysql.createPool({ uri: DB_URL })
使用你的数据库连接字符串创建了一个连接池。连接池会维护一组数据库连接,当你的应用需要执行数据库操作时,它会从池中取出一个可用连接,用完后归还,而不是每次都重新建立连接,这样效率高很多。- 最后,我们把创建好的
pool
对象(而不是DB_URL
字符串)传给drizzle()
。现在 Drizzle 拿到了它认识并且能操作的mysql2
连接池实例,内部就能正确找到promise
相关的方法,错误就解决了。
- 我们引入了
- 代码示例: 上面的代码块就是完整的示例。
- 安全建议:
- 强烈建议 继续使用 SvelteKit 的
$env/dynamic/private
来加载数据库 URL。这能确保你的敏感凭证(用户名、密码)不会暴露到客户端代码中。记得在项目根目录下的.env
文件中配置好DB_URL
。 - 根据你的应用负载和数据库服务器的能力,调整
createPool
中的connectionLimit
等参数。默认值可能不适合生产环境。 - 确保你的数据库用户权限设置合理,遵循最小权限原则。
- 强烈建议 继续使用 SvelteKit 的
方案二:使用 mysql2/promise
创建单个连接
虽然不太推荐在 Web 应用中为每个请求或整个应用生命周期只使用单个连接(因为它容易成为瓶颈,且连接断开后不易自动恢复),但在某些简单脚本或测试场景下可能用到。如果你确定需要单个连接,可以这样做:
-
安装
mysql2
驱动: (同方案一)npm install mysql2
-
修改
src/lib/db/index.ts
:// src/lib/db/index.ts import { drizzle } from 'drizzle-orm/mysql2'; import mysql from 'mysql2/promise'; import { DB_URL } from '$env/dynamic/private'; if (!DB_URL) { throw new Error("数据库连接字符串 'DB_URL' 未在环境变量中设置。"); } // 1. 创建一个 mysql2 单个连接 // 注意:createConnection 通常不适用于需要处理并发请求的 Web 服务器 const connection = mysql.createConnection({ uri: DB_URL, // 其他 mysql2 连接选项 }); // 2. 将单个连接实例传递给 drizzle // Drizzle 也能接受单个连接对象 export const db = drizzle(connection); // (可选) 导出 connection // export const singleConnection = connection;
- 原理和作用:
mysql.createConnection()
创建一个单独的、持久的数据库连接。- Drizzle 同样能识别这种单个连接对象并基于它工作。
- 代码示例: 上面的代码块就是示例。
- 注意事项:
- 不推荐用于 SvelteKit 应用。 单个连接在并发请求下会相互等待,性能差。如果连接因网络问题断开,需要手动处理重连逻辑,比较麻烦。连接池 (
createPool
) 能更好地处理这些问题。 - 如果你确实用了
createConnection
,需要考虑应用关闭时手动断开连接connection.end()
,避免资源泄露。使用连接池则通常不需要手动管理单个连接的关闭。
- 不推荐用于 SvelteKit 应用。 单个连接在并发请求下会相互等待,性能差。如果连接因网络问题断开,需要手动处理重连逻辑,比较麻烦。连接池 (
进阶探讨
理解 Drizzle 的驱动机制
Drizzle ORM 设计上是解耦的,它本身不直接处理网络连接和特定数据库的协议细节。它依赖于各种数据库驱动程序(如 mysql2
, pg
, postgres
, better-sqlite3
等)提供的标准化接口来执行 SQL 查询。
当你调用 drizzle(client)
时,Drizzle 会检查你传入的 client
对象,判断它是什么类型的数据库客户端(通过检查其特定的属性或方法,比如这次出错的 promise
属性),然后使用相应的适配器来与该客户端交互。这就是为什么你必须提供一个实际的驱动实例,而不是简单的配置信息。
SvelteKit 中的数据库连接管理
在 SvelteKit 应用中,数据库连接(尤其是连接池)的初始化时机和管理方式也值得思考。
-
初始化位置:
- 直接放在
src/lib/db/index.ts
中,如上例所示,是常见的做法。这样每次导入db
时都会获得同一个实例(因为 Node.js 模块缓存机制)。 - 另一种方式是在
src/hooks.server.ts
文件中初始化,并将实例附加到event.locals
上。这种方式可以让你在所有服务端钩子、API 路由和页面load
函数中通过event.locals.db
访问,可能更符合 SvelteKit 的数据流管理。
- 直接放在
-
单例模式:
关键是确保你的数据库连接池只被创建一次 。重复创建连接池会消耗不必要的资源。上面的src/lib/db/index.ts
示例利用了 Node.js 模块的缓存特性,自然地实现了单例。每次import { db } from '$lib/db';
都会拿到第一次创建的那个pool
包装后的drizzle
实例。
这里提供一个更健壮的 src/lib/db/index.ts
示例,结合单例模式和稍作封装:
// src/lib/db/index.ts
import { drizzle, MySql2Database } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import { DB_URL } from '$env/dynamic/private';
import * as schema from './schema'; // 假设你的 Drizzle schema 在这里
let dbInstance: MySql2Database<typeof schema> | null = null;
let pool: mysql.Pool | null = null;
function getDbInstance(): MySql2Database<typeof schema> {
if (!dbInstance) {
if (!DB_URL) {
throw new Error("数据库连接字符串 'DB_URL' 未在环境变量中设置。");
}
// 只在第一次调用时创建连接池
if (!pool) {
console.log('正在创建数据库连接池...'); // 方便调试,看是否重复创建
pool = mysql.createPool({
uri: DB_URL,
// 推荐添加配置:
// connectionLimit: 10,
// waitForConnections: true,
// queueLimit: 0,
});
// (可选) 监听连接池事件,例如错误
pool.on('error', (err) => {
console.error('数据库连接池错误:', err);
// 在这里可以添加错误处理逻辑,比如尝试重新连接或记录日志
});
}
// 创建 Drizzle 实例,并传入 schema
// 传入 schema 可以让 Drizzle 实例获得类型提示和关系查询能力
console.log('正在创建 Drizzle 实例...');
dbInstance = drizzle(pool, { schema, mode: 'default' }); // mode: 'default' 是标准模式
}
return dbInstance;
}
// 导出的 db 就是获取单例的函数调用结果
export const db = getDbInstance();
// (可选) 提供一个显式关闭连接池的函数,可以在应用关闭时调用
export async function closeDbConnection() {
if (pool) {
console.log('正在关闭数据库连接池...');
await pool.end();
pool = null;
dbInstance = null; // 清理实例引用
console.log('数据库连接池已关闭。');
}
}
这个版本使用了函数来延迟初始化并确保单例,还加入了 schema
的绑定,让 db
对象更好用(能直接操作定义的表,并且有类型提示)。
现在,无论你在项目的哪个服务端文件导入 db
,你得到的都会是同一个经过 Drizzle 包装的、基于那个唯一连接池的实例。
总结一下
遇到 SvelteKit + Drizzle + MySQL 报 Cannot read properties of undefined (reading 'promise')
错误,多半是因为你直接把数据库连接字符串传给了 drizzle()
函数。正确的做法是:
- 安装
mysql2
库。 - 使用
mysql2/promise
的createPool
(推荐) 或createConnection
方法,根据你的DB_URL
创建一个数据库连接池或单个连接实例。 - 把创建好的连接池/连接实例 对象传递给
drizzle()
函数。 - 推荐在
src/lib/db/index.ts
中实现单例模式来管理数据库连接池的创建。 - 务必使用环境变量管理敏感的数据库凭证。
这样修改后,Drizzle 就能愉快地和你的 MySQL 数据库通过 mysql2
驱动沟通了,那个恼人的 TypeError
也就烟消云散了。