返回

修复 SvelteKit + Drizzle + MySQL 'promise' 未定义错误

mysql

修复 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 创建连接池 (推荐)

这是最常见也是推荐的方式。

  1. 安装 mysql2 驱动:
    如果你还没装,先把它装上:

    npm install mysql2
    # 或者 yarn add mysql2
    # 或者 pnpm add mysql2
    
  2. 修改 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 等参数。默认值可能不适合生产环境。
    • 确保你的数据库用户权限设置合理,遵循最小权限原则。

方案二:使用 mysql2/promise 创建单个连接

虽然不太推荐在 Web 应用中为每个请求或整个应用生命周期只使用单个连接(因为它容易成为瓶颈,且连接断开后不易自动恢复),但在某些简单脚本或测试场景下可能用到。如果你确定需要单个连接,可以这样做:

  1. 安装 mysql2 驱动: (同方案一)

    npm install mysql2
    
  2. 修改 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(),避免资源泄露。使用连接池则通常不需要手动管理单个连接的关闭。

进阶探讨

理解 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() 函数。正确的做法是:

  1. 安装 mysql2 库。
  2. 使用 mysql2/promisecreatePool (推荐) 或 createConnection 方法,根据你的 DB_URL 创建一个数据库连接池或单个连接实例。
  3. 把创建好的连接池/连接实例 对象传递给 drizzle() 函数。
  4. 推荐在 src/lib/db/index.ts 中实现单例模式来管理数据库连接池的创建。
  5. 务必使用环境变量管理敏感的数据库凭证。

这样修改后,Drizzle 就能愉快地和你的 MySQL 数据库通过 mysql2 驱动沟通了,那个恼人的 TypeError 也就烟消云散了。