1. Sequelize:现代Node.js应用的数据层基石
如果你正在用Node.js开发后端服务,尤其是涉及到数据库操作,那么Sequelize这个名字你一定不陌生。它不是一个新潮的框架,但绝对是Node.js生态中处理关系型数据库最成熟、最强大的ORM(对象关系映射)工具之一。我接触Sequelize已经有五六年了,从早期的v3版本一路用到现在的v6,可以说见证了它的成长与变迁。很多新手觉得ORM是“过度设计”,不如直接写SQL来得直接痛快。但当你真正维护一个业务逻辑复杂、表结构繁多、团队协作紧密的项目时,你就会发现,一个设计良好的ORM能帮你省去多少重复劳动,规避多少低级错误。Sequelize的核心价值,就在于它用JavaScript对象和类的方式,为你抽象了数据库表、字段和关系,让你能用更符合编程思维的方式去操作数据,同时又不失灵活性和性能。无论是快速原型开发,还是构建高可维护性的企业级应用,它都是一个绕不开的选择。
2. 核心概念与模型定义:从数据库表到JavaScript类
要玩转Sequelize,第一步必须彻底理解它的几个核心概念:模型(Model)、实例(Instance)和迁移(Migration)。这不仅仅是记住几个API,而是理解Sequelize设计哲学的基础。
2.1 模型定义:连接代码与数据库的桥梁
模型是Sequelize的灵魂,它是对数据库中一张表的抽象描述。定义一个模型,不仅仅是定义字段类型,更是定义业务实体的属性和行为。我们来看一个经典的User用户模型定义:
const { Sequelize, DataTypes } = require('sequelize'); const sequelize = new Sequelize('database', 'username', 'password', { host: 'localhost', dialect: 'mysql' }); const User = sequelize.define('User', { // 主键ID,自增整数,是Sequelize的推荐做法 id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, // 用户名,唯一且不能为空 username: { type: DataTypes.STRING(50), allowNull: false, unique: true, validate: { len: [3, 50] // 内置验证器,确保长度在3到50之间 } }, // 邮箱,有特定的格式验证 email: { type: DataTypes.STRING, allowNull: false, unique: true, validate: { isEmail: true } }, // 状态,使用枚举类型,限定取值范围 status: { type: DataTypes.ENUM('active', 'inactive', 'suspended'), defaultValue: 'active' }, // 元数据,用JSON类型存储灵活的结构化数据 metadata: { type: DataTypes.JSON }, // 创建时间和更新时间,Sequelize默认会管理这两个字段 // 但显式定义可以更清晰地表达意图 createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE }, { // 模型选项 tableName: 'users', // 指定真实的表名,避免自动复数化可能的问题 timestamps: true, // 启用时间戳管理 paranoid: true, // 启用软删除,会新增一个`deletedAt`字段 underscored: true, // 将字段名自动转换为下划线风格(createdAt -> created_at) });这里有几个关键点需要注意。第一是字段类型(DataTypes),它不仅仅是数据库类型的映射,还包含了数据验证的逻辑。比如DataTypes.STRING对应VARCHAR,而validate.isEmail则是在应用层进行的格式校验。第二是模型选项,timestamps和paranoid是我强烈建议开启的选项。timestamps自动管理记录的创建和更新时间,paranoid实现软删除(记录不会被物理删除,只是标记deletedAt),这对于需要数据审计或恢复功能的业务场景至关重要。开启paranoid后,默认的destroy操作会变成软删除,只有调用destroy({ force: true })才会物理删除。
注意:关于表名,Sequelize默认会将模型名转换为复数形式(如
User->Users)。但在生产环境中,我建议总是使用tableName选项显式指定表名。因为自动复数化的规则可能不符合你的数据库命名规范,或者在多语言环境下导致意外行为。
2.2 数据类型与验证:确保数据一致性的第一道防线
Sequelize的DataTypes非常丰富,基本覆盖了所有主流数据库(MySQL, PostgreSQL, SQLite, MariaDB)的常用类型。除了上面例子中的STRING,INTEGER,ENUM,JSON,还有一些需要特别留意的:
DataTypes.TEXT: 用于存储长文本。它有变体TEXT('tiny'),TEXT('medium'),TEXT('long'),对应不同的存储容量,在MySQL中尤其要注意选择。DataTypes.DECIMAL: 用于存储精确小数,如金额。定义时需要指定精度:DataTypes.DECIMAL(10, 2)表示总共10位,小数占2位。DataTypes.VIRTUAL: 虚拟字段,不存在于数据库中,但可以在模型实例上通过getter计算得到。非常适合用于组合字段或衍生计算。DataTypes.UUID: 全局唯一标识符,作为主键时比自增ID更安全,尤其在分布式系统中。
验证器(validate)是另一个强大的功能。它允许你在数据存入数据库前,在应用层进行校验。Sequelize内置了许多验证器,如isEmail,isUrl,isInt,len等。你还可以自定义异步验证函数。但这里有一个常见的“坑”:验证器只在通过Sequelize的create,update,save等方法操作时触发。如果你直接执行原始查询(raw query)或者通过其他途径修改数据,验证器是不会生效的。
2.3 模型同步与数据库迁移:两种管理表结构的方式
定义好模型后,如何让数据库的表结构与之同步?Sequelize提供了两种策略,适用于不同阶段。
1. 模型同步(Model.sync)这是最快捷的方式,常用于开发和原型阶段。
// 强制同步:如果表存在,则先删除再创建(危险!会丢失数据) await User.sync({ force: true }); // 安全同步:仅当表不存在时创建 await User.sync(); // 同步所有模型 await sequelize.sync();sync()方法非常方便,但绝不能在生产环境使用{ force: true },否则分分钟数据清空。即使是不带参数的sync(),在生产环境也需谨慎,因为它可能在你不知情的情况下修改表结构(如新增字段)。我的建议是:在开发初期或自动化测试环境中可以适度使用sync(),一旦项目进入稳定期或上线,必须切换到迁移方案。
2. 数据库迁移(Migration)这是管理数据库结构变更的行业标准做法,类似于Git管理代码版本。Sequelize CLI提供了生成和运行迁移文件的能力。
# 安装CLI npm install --save-dev sequelize-cli # 初始化配置 npx sequelize-cli init # 创建一个创建users表的迁移文件 npx sequelize-cli migration:generate --name create-users-table生成的迁移文件是一个包含up和down方法的脚本。up定义如何应用这次变更,down定义如何回滚。
// migrations/XXXXXXXXXXXXXX-create-users-table.js module.exports = { async up(queryInterface, Sequelize) { await queryInterface.createTable('users', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: Sequelize.STRING, allowNull: false, unique: true }, // ... 其他字段 createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }); // 还可以创建索引 await queryInterface.addIndex('users', ['email']); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable('users'); } };然后通过命令执行迁移或回滚:
npx sequelize-cli db:migrate # 执行迁移 npx sequelize-cli db:migrate:undo # 回滚上一次迁移 npx sequelize-cli db:migrate:undo:all # 回滚所有迁移迁移方案的优势是显而易见的:可追溯、可回滚、适合团队协作和CI/CD流程。对于任何严肃的项目,从第一天起就应该使用迁移来管理数据库结构。
3. 增删改查进阶:超越基础的CRUD操作
掌握了模型定义,接下来就是核心的数据操作。基础的create,findAll,update,destroy大家都会用,但要写出高效、优雅的代码,必须深入了解一些进阶技巧。
3.1 查询:findAll,findOne与强大的where子句
查询是数据库操作中最频繁的部分。Sequelize的查询构建器非常灵活。
基础查询与操作符:
// 1. 查找所有活跃用户 const activeUsers = await User.findAll({ where: { status: 'active' } }); // 2. 使用操作符进行复杂条件查询 const users = await User.findAll({ where: { id: { [Op.gt]: 100, // id > 100 [Op.lte]: 200 // id <= 200 }, username: { [Op.like]: '%john%' // 用户名包含'john' }, [Op.or]: [ // 或条件 { status: 'active' }, { email: { [Op.not]: null } } ] } });Op(Operators)是Sequelize定义的操作符对象,它提供了SQL中几乎所有可能的操作,如Op.eq(等于),Op.ne(不等于),Op.in(在数组中),Op.between(在区间内),Op.and,Op.or等。使用操作符能让你的查询条件表达得更精确。
分页、排序与字段筛选:在实际应用中,我们几乎不会一次性取出所有数据。
const page = 1; const pageSize = 10; const { count, rows } = await User.findAndCountAll({ where: { status: 'active' }, attributes: ['id', 'username', 'email'], // 只选择需要的字段,提升性能 order: [['createdAt', 'DESC']], // 按创建时间倒序 offset: (page - 1) * pageSize, // 跳过多少条 limit: pageSize // 取多少条 }); console.log(`总数:${count}, 当前页数据:`, rows);findAndCountAll在分页场景下特别有用,它在一个查询中同时返回数据和总数(使用COUNT(*) OVER()窗口函数或两个查询,取决于数据库)。注意offset/limit分页在数据量极大时(如offset超过10万)性能会下降,此时应考虑基于游标的分页(where: { id: { [Op.gt]: lastId } })。
3.2 增删改:批量操作、原子更新与软删除
创建与批量创建:
// 创建单个记录 const newUser = await User.create({ username: 'alice', email: 'alice@example.com' }); console.log(newUser.id); // 创建后自动获得自增ID // 批量创建,性能远高于循环调用create const users = await User.bulkCreate([ { username: 'bob', email: 'bob@example.com' }, { username: 'charlie', email: 'charlie@example.com' } ], { validate: true, // 批量操作默认跳过验证,需要显式开启 ignoreDuplicates: true // 忽略重复键错误(如唯一约束冲突) });更新与原子操作:
// 方式1:先查询,再修改实例,最后保存(适合复杂业务逻辑) const user = await User.findByPk(1); if (user) { user.status = 'inactive'; await user.save(); // 会触发实例级别的钩子和验证 } // 方式2:直接使用update方法(更高效,直接生成UPDATE语句) const [affectedCount] = await User.update( { status: 'inactive' }, { where: { id: 1 }, // 返回更新后的记录(PostgreSQL支持,MySQL需配置) returning: true } ); // 原子递增/递减,避免并发问题 await User.increment('loginCount', { by: 1, where: { id: 1 } }); await User.decrement('balance', { by: 100, where: { id: 1 } });对于简单的字段更新,Model.update更高效。而对于需要执行复杂逻辑或依赖更新前数据的场景,instance.save()更合适。increment/decrement是处理计数器、余额等场景的利器,它们在数据库层面执行原子操作,完美解决并发竞争问题。
删除与软删除:
// 物理删除(如果模型未启用paranoid,或使用force) await User.destroy({ where: { id: 1 }, force: true // 强制物理删除,无视paranoid设置 }); // 软删除(模型启用paranoid后,destroy默认是软删除) await User.destroy({ where: { id: 1 } }); // 此时记录还在数据库,但deletedAt字段被设置为当前时间 // 查询时默认排除已软删除的记录 const activeUsers = await User.findAll(); // 查不到id为1的用户 // 查询时包含软删除的记录 const allUsers = await User.findAll({ paranoid: false }); // 恢复软删除的记录 await User.restore({ where: { id: 1 } });软删除是一个极其有用的特性,它让“删除”操作变得可逆,满足了数据安全合规和误操作恢复的需求。务必理解paranoid,destroy,restore以及查询时paranoid: false这几者之间的关系。
3.3 原始查询:当ORM不够用时
尽管ORM强大,但总有复杂查询、存储过程或数据库特有功能是ORM无法完美抽象的。这时就需要原始查询(Raw Query)。
const [results, metadata] = await sequelize.query( 'SELECT * FROM users WHERE status = ? AND DATE(created_at) = ?', { replacements: ['active', '2023-10-01'], // 使用参数替换,防止SQL注入 type: QueryTypes.SELECT // 指定返回类型 } ); // 对于更新操作 await sequelize.query( 'UPDATE users SET login_count = login_count + 1 WHERE id = :userId', { replacements: { userId: 1 }, type: QueryTypes.UPDATE } );重要安全提醒:使用原始查询时,绝对不要使用字符串拼接的方式将变量传入SQL语句。务必使用
replacements或bind参数。这是防止SQL注入攻击的生命线。replacements会将值进行适当的转义和引号包裹,确保安全。
4. 模型关联:处理复杂关系网络
单表操作只是开始,现实中的业务数据充满了关联。Sequelize支持四种核心关联类型,理解它们是用好Sequelize的关键。
4.1 四种核心关联类型详解
假设我们有User(用户),Post(文章),Comment(评论)和Tag(标签)四个模型。
一对一(
hasOne/belongsTo)一个用户有一个个人资料。// User模型中 User.hasOne(models.Profile, { foreignKey: 'userId' }); // Profile模型中 Profile.belongsTo(models.User, { foreignKey: 'userId' });hasOne和belongsTo总是成对出现,区别在于外键放在哪个表。hasOne表示外键在目标模型(Profile)中,belongsTo表示外键在源模型(当前模型)中。在这个例子里,Profile表拥有userId字段,所以User.hasOne(Profile)。一对多(
hasMany/belongsTo)一个用户有多篇文章。// User模型中 User.hasMany(models.Post, { foreignKey: 'authorId' }); // Post模型中 Post.belongsTo(models.User, { foreignKey: 'authorId', as: 'author' });这是最常见的关联。
User.hasMany(Post)表示一个用户拥有多篇文章,外键authorId在Post表中。as: 'author'是为这个关联起一个别名,在查询时特别有用。多对多(
belongsToMany)一篇文章可以有多个标签,一个标签也可以属于多篇文章。// Post模型中 Post.belongsToMany(models.Tag, { through: 'PostTags', // 连接表名 foreignKey: 'postId', otherKey: 'tagId' }); // Tag模型中 Tag.belongsToMany(models.Post, { through: 'PostTags', foreignKey: 'tagId', otherKey: 'postId' });多对多关系需要一个额外的连接表(这里是
PostTags)来存储两个模型的主键对应关系。through选项指定了这个连接表。foreignKey指向当前模型在连接表中的外键,otherKey指向关联模型在连接表中的外键。
4.2 关联查询:include的魔法
定义关联的最大好处就是能进行便捷的关联查询(Eager Loading),使用include选项。
// 查找用户及其所有文章 const userWithPosts = await User.findByPk(1, { include: { model: Post, as: 'posts' // 如果定义关联时用了`as`,这里必须对应 } }); // 访问:userWithPosts.posts // 查找文章及其作者、所有评论和标签 const postWithDetails = await Post.findByPk(1, { include: [ { model: User, as: 'author', attributes: ['id', 'username'] // 只获取作者的部分字段 }, { model: Comment, include: [{ model: User, as: 'commenter' }] // 嵌套include,获取评论的发布者 }, { model: Tag, through: { attributes: [] } // 不获取连接表PostTags的字段 } ] });include可以嵌套,让你通过一次查询就组装出复杂的对象树,这比多次独立查询(N+1查询问题)要高效得多。但也要注意,过度复杂的include可能会导致生成的SQL语句非常庞大,影响性能。对于深层嵌套或数据量大的关联,有时分步查询或使用原始SQL可能是更好的选择。
4.3 关联的创建与操作
关联不仅用于查询,也用于创建有关联的数据。
// 创建用户的同时创建他的个人资料 const user = await User.create({ username: 'david', email: 'david@example.com', Profile: { // 注意这里是大写的模型名 bio: 'A developer' } }, { include: [Profile] // 关键:告诉Sequelize要联级创建Profile }); // 为现有用户添加一篇文章 const user = await User.findByPk(1); const newPost = await Post.create({ title: 'Hello World' }); await user.addPost(newPost); // hasMany关联生成的方法 // 或者使用setPosts替换所有文章 // await user.setPosts([newPost, anotherPost]); // 为文章添加标签(多对多) const post = await Post.findByPk(1); const tag = await Tag.findByPk(100); await post.addTag(tag); // belongsToMany关联生成的方法 // addTag, removeTag, setTags 等方法会自动操作连接表Sequelize会根据你定义的关联,在模型实例上自动添加一系列魔术方法,如getPosts,setPosts,addPost,removePost,createPost等(对于hasMany),极大地简化了关联数据的操作。
5. 钩子、事务与性能优化:生产级应用必备
当你的应用从Demo走向生产,数据的一致性、操作的可靠性和系统的性能就变得至关重要。Sequelize在这些方面也提供了强大的工具。
5.1 生命周期钩子:在关键时刻介入
钩子(Hooks)允许你在模型的生命周期特定节点(如创建前、保存后、删除后等)插入自定义逻辑。这是实现业务规则、数据校验、日志记录的绝佳位置。
const User = sequelize.define('User', { /* ... */ }, { hooks: { // 在创建和更新前,自动哈希密码 beforeSave: async (user, options) => { if (user.changed('password')) { // 检查密码字段是否被修改 const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } }, // 在查询后,移除敏感信息 afterFind: (users, options) => { // users可能是单个实例或数组 if (!Array.isArray(users)) { users = [users]; } users.forEach(user => { if (user) { delete user.dataValues.password; // 从数据值中删除 delete user.password; // 从实例属性中删除 } }); }, // 在软删除后,记录审计日志 afterDestroy: (user, options) => { AuditLog.create({ action: 'USER_SOFT_DELETED', targetId: user.id, details: JSON.stringify(user.dataValues), performedBy: options.transaction?.user?.id // 可以从事务中获取上下文 }); } } });钩子非常强大,但也要谨慎使用。避免在钩子中执行耗时操作(如调用外部API),这会影响所有相关数据库操作的性能。同时,注意钩子函数中this的指向问题,建议始终使用箭头函数或确保正确绑定。
5.2 事务管理:保证数据一致性
事务(Transaction)是将多个数据库操作捆绑成一个原子单元的机制,要么全部成功,要么全部失败。对于转账、订单创建等涉及多表更新的业务,事务是必须的。
// 手动管理事务(推荐,更清晰) const t = await sequelize.transaction(); try { const sender = await User.findByPk(1, { transaction: t, lock: t.LOCK.UPDATE }); const receiver = await User.findByPk(2, { transaction: t, lock: t.LOCK.UPDATE }); if (sender.balance < 100) { throw new Error('余额不足'); } await sender.decrement('balance', { by: 100, transaction: t }); await receiver.increment('balance', { by: 100, transaction: t }); await TransactionRecord.create({ from: sender.id, to: receiver.id, amount: 100 }, { transaction: t }); await t.commit(); // 提交事务 console.log('转账成功'); } catch (error) { await t.rollback(); // 回滚事务 console.error('转账失败,已回滚:', error); } // 自动管理事务(使用CLS或async/await包装) const result = await sequelize.transaction(async (t) => { // 在这个回调函数中,所有操作会自动关联事务`t` const user = await User.create({ username: 'foo' }, { transaction: t }); await Profile.create({ userId: user.id }, { transaction: t }); return user; }); // 如果回调函数执行成功,事务自动提交;如果抛出错误,事务自动回滚。关键点:
- 传递事务对象:在事务内执行的每一个Sequelize方法调用,都必须通过
{ transaction: t }选项传入事务对象。 - 锁:在高并发场景下,为了预防竞态条件,可以使用
lock选项(如t.LOCK.UPDATE行级锁)来锁定要修改的记录。 - 自动事务:
sequelize.transaction(async (t) => { ... })的写法更简洁,利用了Async/Await,但要注意错误处理。
5.3 性能优化与常见陷阱
即使使用了ORM,性能问题依然需要关注。
1. N+1查询问题这是ORM最常见的性能陷阱。
// 糟糕的写法:查询所有文章,然后为每篇文章单独查询作者(N+1次查询) const posts = await Post.findAll(); for (const post of posts) { const author = await post.getAuthor(); // 每次循环都发起一次查询! } // 正确的写法:使用include进行预加载(Eager Loading),1-2次查询搞定 const posts = await Post.findAll({ include: [{ model: User, as: 'author' }] });2. 只选择需要的字段避免使用SELECT *。
// 不好 const users = await User.findAll(); // 好 const users = await User.findAll({ attributes: ['id', 'username', 'createdAt'] });3. 合理使用索引Sequelize不会自动为你创建数据库索引。对于经常用于where,order,join条件的字段,应在迁移文件中手动创建索引。
// 在迁移文件的up方法中 await queryInterface.addIndex('users', ['email']); await queryInterface.addIndex('posts', ['authorId', 'createdAt']);4. 分页优化对于深度分页(offset很大),考虑使用where id > lastId的方式(游标分页),而不是limit offset。5. 连接池配置确保Sequelize连接池配置合理,避免连接数不足或过多。
const sequelize = new Sequelize(/* ... */, { pool: { max: 20, // 最大连接数 min: 5, // 最小连接数 acquire: 30000, // 获取连接超时时间(ms) idle: 10000 // 连接空闲超时时间(ms) } });6. 实战:构建一个简单的用户-文章系统
让我们把上面的知识点串联起来,构建一个包含用户、文章、评论和标签的简单系统。这里重点展示模型定义、关联和典型查询。
6.1 模型定义与关联
首先,定义所有模型并建立关联。通常在一个单独的文件(如models/index.js)中集中处理。
// models/user.js module.exports = (sequelize, DataTypes) => { const User = sequelize.define('User', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, email: { type: DataTypes.STRING, unique: true, allowNull: false, validate: { isEmail: true } } }, { timestamps: true }); User.associate = (models) => { User.hasMany(models.Post, { foreignKey: 'authorId', as: 'posts' }); User.hasMany(models.Comment, { foreignKey: 'commenterId', as: 'comments' }); User.hasOne(models.Profile, { foreignKey: 'userId' }); }; return User; }; // models/post.js module.exports = (sequelize, DataTypes) => { const Post = sequelize.define('Post', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, title: { type: DataTypes.STRING, allowNull: false }, content: { type: DataTypes.TEXT } }, { timestamps: true }); Post.associate = (models) => { Post.belongsTo(models.User, { foreignKey: 'authorId', as: 'author' }); Post.hasMany(models.Comment, { foreignKey: 'postId', as: 'comments' }); Post.belongsToMany(models.Tag, { through: 'PostTags', foreignKey: 'postId', otherKey: 'tagId', as: 'tags' }); }; return Post; }; // models/comment.js, models/tag.js, models/profile.js 类似定义... // 然后在 models/index.js 中统一导入并调用 associate6.2 典型业务查询示例
场景1:展示首页文章列表(分页, 包含作者和标签)
async function getHomepagePosts(page = 1, size = 10) { const { count, rows: posts } = await Post.findAndCountAll({ attributes: ['id', 'title', 'createdAt'], include: [ { model: User, as: 'author', attributes: ['id', 'username'] }, { model: Tag, as: 'tags', attributes: ['id', 'name'], through: { attributes: [] } } ], order: [['createdAt', 'DESC']], offset: (page - 1) * size, limit: size, distinct: true // 使用include进行多对多关联时,分页计数需要distinct }); return { total: count, posts }; }场景2:创建一篇带标签的新文章
async function createPostWithTags(authorId, postData, tagNames) { const t = await sequelize.transaction(); try { // 1. 创建文章 const post = await Post.create({ ...postData, authorId }, { transaction: t }); // 2. 查找或创建标签 const tagPromises = tagNames.map(name => Tag.findOrCreate({ where: { name }, defaults: { name }, transaction: t }) ); const tagResults = await Promise.all(tagPromises); const tags = tagResults.map(result => result[0]); // findOrCreate返回[instance, created] // 3. 建立文章和标签的关联 await post.setTags(tags, { transaction: t }); await t.commit(); return post; } catch (error) { await t.rollback(); throw error; // 将错误抛给上层处理 } }这个例子综合运用了事务、findOrCreate、多对多关联操作,是一个在生产中很常见的模式。
6.3 配置与连接管理实践
最后,一个健壮的Sequelize配置对于生产环境至关重要。我通常会创建一个config/database.js文件来管理不同环境的配置,并使用dotenv管理敏感信息。
// config/database.js require('dotenv').config(); // 从.env文件加载环境变量 module.exports = { development: { username: process.env.DB_USER || 'root', password: process.env.DB_PASS || null, database: process.env.DB_NAME || 'myapp_dev', host: process.env.DB_HOST || '127.0.0.1', port: process.env.DB_PORT || 3306, dialect: 'mysql', logging: console.log, // 开发环境显示SQL日志 pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } }, test: { username: process.env.DB_TEST_USER || 'root', // ... 类似,可能使用内存数据库如sqlite dialect: 'sqlite', storage: ':memory:', logging: false }, production: { username: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, host: process.env.DB_HOST, port: process.env.DB_PORT, dialect: 'mysql', logging: false, // 生产环境关闭SQL日志,避免敏感信息泄露和性能开销 pool: { max: 20, min: 5, acquire: 30000, idle: 10000 }, // 生产环境连接池更大 dialectOptions: { ssl: { // 如果数据库要求SSL连接 require: true, rejectUnauthorized: false // 根据你的CA证书情况调整 } } } };然后在主应用文件中初始化Sequelize实例:
// app.js 或 database.js const { Sequelize } = require('sequelize'); const env = process.env.NODE_ENV || 'development'; const config = require('./config/database')[env]; const sequelize = new Sequelize(config.database, config.username, config.password, config); // 测试连接 (async () => { try { await sequelize.authenticate(); console.log('数据库连接成功.'); } catch (error) { console.error('无法连接到数据库:', error); } })();这套配置分离了环境,安全地管理了凭证,并设置了合理的连接池参数,是部署到生产环境的基础。记住,永远不要将数据库密码等敏感信息硬编码在代码中。