Sequelize 数据库迁移详解 - 版本化管理数据库结构
发布时间:2024-03-12
作者:一介布衣
标签:Sequelize, 数据库迁移, 版本控制, 数据库管理
前言
今天咱们来学习 Sequelize 的迁移(Migration)系统。说实话,迁移是团队开发中必不可少的工具,它让数据库结构的变更变得可控、可追溯。
我记得刚开始做项目的时候,每次数据库结构有变化,都要手动在各个环境执行 SQL 语句,经常出现开发环境和生产环境不一致的问题。有时候忘记在某个环境执行了某个 SQL,就会导致各种奇怪的错误。后来学会了迁移,这些问题就彻底解决了。
今天我就把迁移的概念、用法和最佳实践分享给大家。
什么是数据库迁移?
数据库迁移是一种版本化管理数据库结构变更的方法。每个迁移文件都包含了:
- up 方法:执行变更(如创建表、添加字段等)
- down 方法:回滚变更(撤销 up 方法的操作)
这样我们就能够:
- 跟踪数据库结构的每一次变更
- 在不同环境间同步数据库结构
- 回滚到任意历史版本
- 团队协作时避免数据库冲突
安装和配置 Sequelize CLI
1. 安装 CLI 工具
bash
# 全局安装
npm install -g sequelize-cli
# 或者项目内安装
npm install --save-dev sequelize-cli
2. 初始化项目
bash
# 初始化 Sequelize 项目结构
npx sequelize-cli init
这会创建以下目录结构:
project/
├── config/
│ └── config.json # 数据库配置
├── migrations/ # 迁移文件目录
├── models/ # 模型文件目录
│ └── index.js
└── seeders/ # 种子数据目录
3. 配置数据库连接
编辑 config/config.json
:
json
{
"development": {
"username": "root",
"password": "password",
"database": "myapp_development",
"host": "127.0.0.1",
"dialect": "mysql",
"logging": true
},
"test": {
"username": "root",
"password": "password",
"database": "myapp_test",
"host": "127.0.0.1",
"dialect": "mysql",
"logging": false
},
"production": {
"username": "root",
"password": "password",
"database": "myapp_production",
"host": "127.0.0.1",
"dialect": "mysql",
"logging": false
}
}
或者使用 JavaScript 配置文件 config/config.js
:
javascript
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USER || 'root',
password: process.env.DB_PASS || 'password',
database: process.env.DB_NAME || 'myapp_development',
host: process.env.DB_HOST || '127.0.0.1',
dialect: 'mysql',
logging: console.log
},
test: {
username: process.env.DB_USER || 'root',
password: process.env.DB_PASS || 'password',
database: process.env.DB_NAME_TEST || 'myapp_test',
host: process.env.DB_HOST || '127.0.0.1',
dialect: 'mysql',
logging: false
},
production: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
dialect: 'mysql',
logging: false,
pool: {
max: 10,
min: 0,
acquire: 30000,
idle: 10000
}
}
};
创建迁移文件
1. 创建表的迁移
bash
# 创建用户表迁移
npx sequelize-cli migration:generate --name create-users
这会生成一个迁移文件 migrations/20240312000000-create-users.js
:
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
firstName: {
type: Sequelize.STRING(50),
allowNull: false
},
lastName: {
type: Sequelize.STRING(50),
allowNull: false
},
email: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true
},
password: {
type: Sequelize.STRING(255),
allowNull: false
},
isActive: {
type: Sequelize.BOOLEAN,
defaultValue: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
// 添加索引
await queryInterface.addIndex('Users', ['email']);
await queryInterface.addIndex('Users', ['isActive']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users');
}
};
2. 添加字段的迁移
bash
# 添加用户头像字段
npx sequelize-cli migration:generate --name add-avatar-to-users
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('Users', 'avatar', {
type: Sequelize.STRING(500),
allowNull: true,
comment: '用户头像URL'
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'avatar');
}
};
3. 修改字段的迁移
bash
# 修改邮箱字段长度
npx sequelize-cli migration:generate --name modify-email-length
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.changeColumn('Users', 'email', {
type: Sequelize.STRING(150), // 从100改为150
allowNull: false,
unique: true
});
},
async down(queryInterface, Sequelize) {
await queryInterface.changeColumn('Users', 'email', {
type: Sequelize.STRING(100), // 回滚到原来的长度
allowNull: false,
unique: true
});
}
};
4. 添加索引的迁移
bash
# 添加复合索引
npx sequelize-cli migration:generate --name add-name-index
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
// 添加复合索引
await queryInterface.addIndex('Users', ['firstName', 'lastName'], {
name: 'users_name_index'
});
// 添加唯一索引
await queryInterface.addIndex('Users', ['email'], {
unique: true,
name: 'users_email_unique'
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeIndex('Users', 'users_name_index');
await queryInterface.removeIndex('Users', 'users_email_unique');
}
};
执行迁移
1. 基本命令
bash
# 执行所有待执行的迁移
npx sequelize-cli db:migrate
# 回滚最后一次迁移
npx sequelize-cli db:migrate:undo
# 回滚到指定迁移
npx sequelize-cli db:migrate:undo:all --to 20240312000000-create-users.js
# 回滚所有迁移
npx sequelize-cli db:migrate:undo:all
# 查看迁移状态
npx sequelize-cli db:migrate:status
2. 指定环境
bash
# 在生产环境执行迁移
NODE_ENV=production npx sequelize-cli db:migrate
# 在测试环境执行迁移
NODE_ENV=test npx sequelize-cli db:migrate
复杂迁移场景
1. 数据迁移
有时候我们不仅要修改表结构,还要迁移数据:
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
// 1. 添加新字段
await queryInterface.addColumn('Users', 'fullName', {
type: Sequelize.STRING(100),
allowNull: true
});
// 2. 迁移数据:将 firstName 和 lastName 合并为 fullName
await queryInterface.sequelize.query(`
UPDATE Users
SET fullName = CONCAT(firstName, ' ', lastName)
WHERE firstName IS NOT NULL AND lastName IS NOT NULL
`);
// 3. 设置字段为非空
await queryInterface.changeColumn('Users', 'fullName', {
type: Sequelize.STRING(100),
allowNull: false
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'fullName');
}
};
2. 重命名表和字段
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
// 重命名表
await queryInterface.renameTable('Users', 'Members');
// 重命名字段
await queryInterface.renameColumn('Members', 'isActive', 'status');
// 修改字段类型
await queryInterface.changeColumn('Members', 'status', {
type: Sequelize.ENUM('active', 'inactive', 'suspended'),
allowNull: false,
defaultValue: 'active'
});
},
async down(queryInterface, Sequelize) {
// 回滚字段类型
await queryInterface.changeColumn('Members', 'status', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
});
// 回滚字段名
await queryInterface.renameColumn('Members', 'status', 'isActive');
// 回滚表名
await queryInterface.renameTable('Members', 'Users');
}
};
3. 外键关系迁移
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
// 创建文章表
await queryInterface.createTable('Posts', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
type: Sequelize.STRING(200),
allowNull: false
},
content: {
type: Sequelize.TEXT,
allowNull: false
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
// 添加外键索引
await queryInterface.addIndex('Posts', ['userId']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Posts');
}
};
种子数据(Seeders)
1. 创建种子文件
bash
# 创建用户种子数据
npx sequelize-cli seed:generate --name demo-users
javascript
'use strict';
const bcrypt = require('bcryptjs');
module.exports = {
async up(queryInterface, Sequelize) {
const users = [
{
firstName: '张',
lastName: '三',
email: 'zhangsan@example.com',
password: await bcrypt.hash('123456', 10),
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
},
{
firstName: '李',
lastName: '四',
email: 'lisi@example.com',
password: await bcrypt.hash('123456', 10),
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
}
];
await queryInterface.bulkInsert('Users', users, {});
},
async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete('Users', {
email: ['zhangsan@example.com', 'lisi@example.com']
}, {});
}
};
2. 执行种子数据
bash
# 执行所有种子文件
npx sequelize-cli db:seed:all
# 执行特定种子文件
npx sequelize-cli db:seed --seed 20240312000000-demo-users.js
# 回滚所有种子数据
npx sequelize-cli db:seed:undo:all
# 回滚特定种子数据
npx sequelize-cli db:seed:undo --seed 20240312000000-demo-users.js
迁移最佳实践
1. 迁移文件命名规范
bash
# 好的命名
20240312120000-create-users.js
20240312120100-add-avatar-to-users.js
20240312120200-create-posts.js
20240312120300-add-foreign-key-posts-users.js
# 避免的命名
migration1.js
update.js
fix.js
2. 原子性操作
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// 所有操作在一个事务中执行
await queryInterface.addColumn('Users', 'phone', {
type: Sequelize.STRING(20),
allowNull: true
}, { transaction });
await queryInterface.addIndex('Users', ['phone'], {
name: 'users_phone_index'
}, { transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeIndex('Users', 'users_phone_index', { transaction });
await queryInterface.removeColumn('Users', 'phone', { transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};
3. 数据备份和验证
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
// 1. 备份数据
await queryInterface.sequelize.query(`
CREATE TABLE Users_backup AS SELECT * FROM Users
`);
try {
// 2. 执行迁移
await queryInterface.addColumn('Users', 'newField', {
type: Sequelize.STRING(100),
allowNull: true
});
// 3. 数据迁移
await queryInterface.sequelize.query(`
UPDATE Users SET newField = CONCAT('prefix_', id)
`);
// 4. 验证数据
const [results] = await queryInterface.sequelize.query(`
SELECT COUNT(*) as count FROM Users WHERE newField IS NULL
`);
if (results[0].count > 0) {
throw new Error('数据迁移验证失败');
}
// 5. 删除备份表
await queryInterface.sequelize.query('DROP TABLE Users_backup');
} catch (error) {
// 恢复数据
await queryInterface.sequelize.query('DROP TABLE IF EXISTS Users');
await queryInterface.sequelize.query('RENAME TABLE Users_backup TO Users');
throw error;
}
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'newField');
}
};
生产环境迁移策略
1. 零停机迁移
javascript
// 第一步:添加新字段(允许为空)
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('Users', 'newEmail', {
type: Sequelize.STRING(150),
allowNull: true // 先允许为空
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'newEmail');
}
};
// 第二步:应用代码同时写入新旧字段
// 第三步:迁移历史数据
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
UPDATE Users SET newEmail = email WHERE newEmail IS NULL
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
UPDATE Users SET newEmail = NULL
`);
}
};
// 第四步:设置新字段为非空,删除旧字段
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.changeColumn('Users', 'newEmail', {
type: Sequelize.STRING(150),
allowNull: false
});
await queryInterface.removeColumn('Users', 'email');
await queryInterface.renameColumn('Users', 'newEmail', 'email');
},
async down(queryInterface, Sequelize) {
await queryInterface.renameColumn('Users', 'email', 'newEmail');
await queryInterface.addColumn('Users', 'email', {
type: Sequelize.STRING(100),
allowNull: false
});
}
};
2. 迁移监控
javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
console.log('开始执行迁移...');
const startTime = Date.now();
try {
await queryInterface.addColumn('Users', 'lastLoginAt', {
type: Sequelize.DATE,
allowNull: true
});
const duration = Date.now() - startTime;
console.log(`迁移完成,耗时: ${duration}ms`);
} catch (error) {
console.error('迁移失败:', error);
throw error;
}
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'lastLoginAt');
}
};
团队协作规范
1. 迁移文件管理
bash
# .gitignore 不要忽略迁移文件
# migrations/ 目录应该被版本控制
# 团队成员拉取代码后执行
git pull
npm install
npx sequelize-cli db:migrate
2. 冲突解决
当多个开发者同时创建迁移文件时,可能会出现时间戳冲突:
bash
# 重命名迁移文件,调整时间戳
mv 20240312120000-add-field.js 20240312120001-add-field.js
3. 迁移检查脚本
javascript
// scripts/check-migrations.js
const { exec } = require('child_process');
async function checkMigrations() {
return new Promise((resolve, reject) => {
exec('npx sequelize-cli db:migrate:status', (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
if (stdout.includes('down')) {
console.error('发现未执行的迁移文件,请先执行迁移');
process.exit(1);
}
console.log('所有迁移文件已执行');
resolve();
});
});
}
checkMigrations().catch(console.error);
总结
今天我们深入学习了 Sequelize 的迁移系统:
- ✅ 迁移的基本概念和重要性
- ✅ CLI 工具的安装和配置
- ✅ 各种迁移场景的实现方法
- ✅ 种子数据的使用
- ✅ 生产环境迁移策略
- ✅ 团队协作最佳实践
掌握了这些知识,你就能够:
- 版本化管理数据库结构变更
- 在团队中安全地协作开发
- 实现零停机的数据库升级
- 构建可靠的数据库部署流程
下一篇文章,我们将学习 Sequelize 的性能优化技巧,让你的应用跑得更快!
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!