# 后端数据库迁移文档 ## 文档信息 - **文档版本**: V1.0 - **创建日期**: 2026-01-26 - **文档类型**: 后端数据库迁移文档 --- ## 1. 数据库迁移概述 ### 1.1 什么是数据库迁移 数据库迁移是一种管理数据库模式变更的方法,它允许您: - 版本化数据库模式 - 跟踪数据库变更历史 - 在不同环境之间一致地应用数据库变更 - 回滚错误的数据库变更 - 协作开发数据库模式 ### 1.2 为什么使用数据库迁移 - **可追溯性**: 记录所有数据库变更的历史 - **一致性**: 在开发、测试和生产环境中应用相同的变更 - **安全性**: 提供回滚机制,防止错误变更导致的数据丢失 - **协作**: 团队成员可以共享和审查数据库变更 - **自动化**: 简化数据库部署流程 --- ## 2. 迁移工具 ### 2.1 Alembic 本项目使用 **Alembic** 作为数据库迁移工具。Alembic 是 SQLAlchemy 的官方数据库迁移工具,提供了以下功能: - 自动生成迁移脚本 - 管理迁移版本 - 应用和回滚迁移 - 支持多种数据库 - 与 SQLAlchemy 无缝集成 ### 2.2 安装 Alembic Alembic 已经在项目依赖中包含,使用以下命令安装: ```bash # 使用 Poetry poetry install # 使用 pip pip install -r requirements.txt ``` --- ## 3. 迁移配置 ### 3.1 初始化 Alembic 如果项目还没有初始化 Alembic,使用以下命令: ```bash # 在项目根目录执行 alembic init alembic ``` ### 3.2 配置文件 Alembic 的主要配置文件是 `alembic.ini`,位于项目根目录: ```ini # alembic.ini # A generic, single database configuration. [alembic] # path to migration scripts script_location = alembic # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time # file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. # If specified, requires the python-dateutil library # timezone = # max length of characters to apply to the # "slug" field # truncate_slug_length = 40 # set to 'true' to run the environment during # the 'revision' command, regardless of autogenerate # revision_environment = false # set to 'true' to allow .pyc and .pyo files without # a source .py file to be detected as revisions in the # versions/ directory # sourceless = false # version location specification; This defaults # to alembic/versions. When using multiple version # directories, initial revisions must be specified with --version-path. # The path separator used here should be the separator specified by "version_path_separator" below. # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions # version path separator; As mentioned above, this is the character used to split # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. # Valid values for version_path_separator are: # # version_path_separator = : # version_path_separator = ; # version_path_separator = space version_path_separator = os # Use os.pathsep. # the output encoding used when revision files # are written from script.py.mako # output_encoding = utf-8 sqlalchemy.url = mysql+pymysql://username:password@localhost:3306/project_management [post_write_hooks] # post_write_hooks defines scripts or Python functions that are run # on newly generated revision scripts. See the documentation for further # detail and examples # format using "black" - use the console_scripts runner, against the "black" entrypoint # hooks = black # black.type = console_scripts # black.entrypoint = black # black.options = -l 79 REVISION_SCRIPT_FILENAME # Logging configuration [loggers] keys = root,sqlalchemy,alembic [handlers] keys = console [formatters] keys = generic [logger_root] level = WARN handlers = console qualname = [logger_sqlalchemy] level = WARN handlers = qualname = sqlalchemy.engine [logger_alembic] level = INFO handlers = qualname = alembic [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S ``` ### 3.3 环境配置 Alembic 的环境配置文件是 `alembic/env.py`,用于配置数据库连接和迁移行为: ```python # alembic/env.py from logging.config import fileConfig from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context import os import sys from pathlib import Path # 将项目根目录添加到 Python 路径 sys.path.append(str(Path(__file__).parent.parent)) # 导入模型和配置 from app.config import settings from app.database.base import Base from app.models import * # 导入所有模型 # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # 使用环境变量中的数据库 URL config.set_main_option('sqlalchemy.url', settings.database_url) # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. def run_migrations_offline() -> None: """Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable here as well. By skipping the Engine creation we don't even need a DBAPI to be available. Calls to context.execute() here emit the given string to the script output. """ url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations() def run_migrations_online() -> None: """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ connectable = engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool, ) with connectable.connect() as connection: context.configure( connection=connection, target_metadata=target_metadata ) with context.begin_transaction(): context.run_migrations() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() ``` ### 3.4 脚本模板 Alembic 使用 `script.py.mako` 作为迁移脚本的模板,位于 `alembic` 目录: ```mako """${message} Revision ID: ${up_revision} Revises: ${down_revision | comma,n} Create Date: ${create_date} """ from alembic import op import sqlalchemy as sa ${imports if imports else ""} # revision identifiers, used by Alembic. revision = ${repr(up_revision)} down_revision = ${repr(down_revision)} branch_labels = ${repr(branch_labels)} depends_on = ${repr(depends_on)} def upgrade() -> None: ${upgrades if upgrades else "pass"} def downgrade() -> None: ${downgrades if downgrades else "pass"} ``` --- ## 4. 迁移操作 ### 4.1 生成迁移脚本 #### 自动生成迁移脚本 当您修改了模型定义后,使用以下命令自动生成迁移脚本: ```bash # 在项目根目录执行 alembic revision --autogenerate -m "描述迁移内容" # 示例 alembic revision --autogenerate -m "创建用户表" ``` #### 手动创建迁移脚本 如果需要手动创建迁移脚本,使用以下命令: ```bash # 在项目根目录执行 alembic revision -m "描述迁移内容" # 示例 alembic revision -m "添加用户状态字段" ``` 然后编辑生成的迁移脚本,添加具体的迁移操作。 ### 4.2 应用迁移 #### 应用所有迁移 使用以下命令将所有未应用的迁移应用到数据库: ```bash # 在项目根目录执行 alembic upgrade head ``` #### 应用特定迁移 使用以下命令将迁移应用到特定版本: ```bash # 在项目根目录执行 alembic upgrade # 示例 alembic upgrade 1234abcd ``` ### 4.3 回滚迁移 #### 回滚到上一个版本 使用以下命令回滚到上一个迁移版本: ```bash # 在项目根目录执行 alembic downgrade -1 ``` #### 回滚到特定版本 使用以下命令回滚到特定迁移版本: ```bash # 在项目根目录执行 alembic downgrade # 示例 alembic downgrade 1234abcd ``` #### 回滚到初始状态 使用以下命令回滚到初始状态: ```bash # 在项目根目录执行 alembic downgrade base ``` ### 4.4 查看迁移状态 使用以下命令查看当前的迁移状态: ```bash # 在项目根目录执行 alembic current ``` 使用以下命令查看所有迁移版本: ```bash # 在项目根目录执行 alembic history # 显示更详细的信息 alembic history --verbose # 显示图形化的迁移树 alembic history --graph ``` --- ## 5. 迁移最佳实践 ### 5.1 迁移脚本管理 1. **清晰的迁移消息**: 使用描述性的迁移消息,说明迁移的目的 2. **版本控制**: 将迁移脚本纳入版本控制 3. **测试迁移**: 在应用到生产环境之前,在测试环境中测试迁移 4. **备份数据**: 在应用迁移之前,备份数据库 5. **逐步迁移**: 对于大型迁移,考虑分步骤进行 ### 5.2 迁移脚本编写 1. **保持迁移脚本简单**: 每个迁移脚本只包含一个逻辑变更 2. **处理默认值**: 为新添加的列提供合理的默认值 3. **处理数据迁移**: 如果需要迁移数据,在迁移脚本中添加相应的代码 4. **处理约束**: 注意外键约束和唯一约束的处理 5. **编写回滚脚本**: 确保每个迁移都有对应的回滚操作 ### 5.3 常见迁移操作 #### 添加表 ```python def upgrade() -> None: op.create_table( 'users', sa.Column('id', sa.Integer(), nullable=False), sa.Column('username', sa.String(length=50), nullable=False), sa.Column('password', sa.String(length=128), nullable=False), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('username') ) def downgrade() -> None: op.drop_table('users') ``` #### 添加列 ```python def upgrade() -> None: op.add_column( 'users', sa.Column('email', sa.String(length=100), nullable=True) ) def downgrade() -> None: op.drop_column('users', 'email') ``` #### 修改列 ```python def upgrade() -> None: op.alter_column( 'users', 'email', existing_type=sa.String(length=100), nullable=False ) def downgrade() -> None: op.alter_column( 'users', 'email', existing_type=sa.String(length=100), nullable=True ) ``` #### 添加索引 ```python def upgrade() -> None: op.create_index( op.f('ix_users_email'), 'users', ['email'], unique=True ) def downgrade() -> None: op.drop_index(op.f('ix_users_email'), table_name='users') ``` --- ## 6. 数据库初始化 ### 6.1 初始迁移 当创建新项目时,需要执行初始迁移: 1. **创建模型**: 定义所有数据库模型 2. **生成初始迁移**: `alembic revision --autogenerate -m "初始迁移"` 3. **应用初始迁移**: `alembic upgrade head` ### 6.2 数据种子 在应用初始迁移后,可能需要添加一些初始数据,例如管理员用户: ```python # 在 app/database/seed.py 中 from sqlalchemy.orm import Session from app.models.user import User from app.database.session import get_db from passlib.context import CryptContext pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def seed_data(db: Session) -> None: """添加初始数据""" # 检查是否已有管理员用户 admin_user = db.query(User).filter(User.username == "admin").first() if not admin_user: # 创建管理员用户 admin_user = User( username="admin", password=pwd_context.hash("123456"), real_name="系统管理员", department="ADMIN", role="ADMIN", status=1 ) db.add(admin_user) # 检查是否已有市场部用户 marketing_user = db.query(User).filter(User.username == "marketing").first() if not marketing_user: # 创建市场部用户 marketing_user = User( username="marketing", password=pwd_context.hash("123456"), real_name="市场部经理", department="MARKETING", role="MARKETING", status=1 ) db.add(marketing_user) # 检查是否已有其他部门用户 other_user = db.query(User).filter(User.username == "other").first() if not other_user: # 创建其他部门用户 other_user = User( username="other", password=pwd_context.hash("123456"), real_name="技术部经理", department="TECHNOLOGY", role="OTHER", status=1 ) db.add(other_user) db.commit() if __name__ == "__main__": db = next(get_db()) try: seed_data(db) print("数据种子添加成功") finally: db.close() ``` 执行数据种子脚本: ```bash # 在项目根目录执行 python -m app.database.seed ``` --- ## 7. 迁移示例 ### 7.1 示例 1: 创建用户表 **迁移脚本**: `alembic/versions/1234abcd_create_user_table.py` ```python """创建用户表 Revision ID: 1234abcd Revises: Create Date: 2025-01-01 00:00:00.000000 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '1234abcd' down_revision = None branch_labels = None depends_on = None def upgrade() -> None: op.create_table('sys_user', sa.Column('user_id', sa.String(length=32), nullable=False), sa.Column('username', sa.String(length=50), nullable=False), sa.Column('password', sa.String(length=128), nullable=False), sa.Column('real_name', sa.String(length=50), nullable=False), sa.Column('department', sa.String(length=50), nullable=False), sa.Column('phone', sa.String(length=20), nullable=True), sa.Column('email', sa.String(length=100), nullable=True), sa.Column('role', sa.String(length=20), nullable=False), sa.Column('status', sa.Integer(), nullable=False), sa.Column('create_time', sa.DateTime(), nullable=False), sa.Column('update_time', sa.DateTime(), nullable=False), sa.Column('last_login_time', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('user_id'), sa.UniqueConstraint('username') ) op.create_index(op.f('ix_sys_user_department'), 'sys_user', ['department'], unique=False) op.create_index(op.f('ix_sys_user_role'), 'sys_user', ['role'], unique=False) op.create_index(op.f('ix_sys_user_status'), 'sys_user', ['status'], unique=False) def downgrade() -> None: op.drop_index(op.f('ix_sys_user_status'), table_name='sys_user') op.drop_index(op.f('ix_sys_user_role'), table_name='sys_user') op.drop_index(op.f('ix_sys_user_department'), table_name='sys_user') op.drop_table('sys_user') ``` ### 7.2 示例 2: 创建项目表 **迁移脚本**: `alembic/versions/5678efgh_create_project_table.py` ```python """创建项目表 Revision ID: 5678efgh Revises: 1234abcd Create Date: 2025-01-02 00:00:00.000000 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '5678efgh' down_revision = '1234abcd' branch_labels = None depends_on = None def upgrade() -> None: op.create_table('project', sa.Column('project_id', sa.String(length=32), nullable=False), sa.Column('project_no', sa.String(length=20), nullable=False), sa.Column('project_name', sa.String(length=200), nullable=False), sa.Column('status', sa.String(length=20), nullable=False), sa.Column('create_time', sa.DateTime(), nullable=False), sa.Column('creator', sa.String(length=50), nullable=False), sa.Column('update_time', sa.DateTime(), nullable=False), sa.Column('last_modifier', sa.String(length=50), nullable=True), sa.Column('leader', sa.String(length=50), nullable=False), sa.Column('phone', sa.String(length=20), nullable=True), sa.Column('email', sa.String(length=100), nullable=True), sa.Column('background', sa.Text(), nullable=True), sa.Column('goal', sa.Text(), nullable=True), sa.Column('scope', sa.Text(), nullable=True), sa.Column('start_date', sa.Date(), nullable=False), sa.Column('planned_end_date', sa.Date(), nullable=False), sa.Column('actual_end_date', sa.Date(), nullable=True), sa.Column('total_budget', sa.Numeric(precision=15, scale=2), nullable=False), sa.Column('used_budget', sa.Numeric(precision=15, scale=2), nullable=False), sa.Column('remaining_budget', sa.Numeric(precision=15, scale=2), nullable=False), sa.Column('remarks', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('project_id'), sa.UniqueConstraint('project_no') ) op.create_index(op.f('ix_project_create_time'), 'project', ['create_time'], unique=False) op.create_index(op.f('ix_project_creator'), 'project', ['creator'], unique=False) op.create_index(op.f('ix_project_leader'), 'project', ['leader'], unique=False) op.create_index(op.f('ix_project_status'), 'project', ['status'], unique=False) op.create_index(op.f('ix_project_update_time'), 'project', ['update_time'], unique=False) def downgrade() -> None: op.drop_index(op.f('ix_project_update_time'), table_name='project') op.drop_index(op.f('ix_project_status'), table_name='project') op.drop_index(op.f('ix_project_leader'), table_name='project') op.drop_index(op.f('ix_project_creator'), table_name='project') op.drop_index(op.f('ix_project_create_time'), table_name='project') op.drop_table('project') ``` --- ## 8. 常见问题 ### 8.1 迁移失败 **问题**:迁移应用失败,出现错误 **解决方法**: 1. 查看错误信息,了解失败原因 2. 检查迁移脚本是否正确 3. 检查数据库连接是否正常 4. 如果需要,回滚到之前的版本并修复问题 ### 8.2 自动生成的迁移脚本不正确 **问题**:Alembic 自动生成的迁移脚本与预期不符 **解决方法**: 1. 检查模型定义是否正确 2. 手动编辑生成的迁移脚本 3. 确保所有模型都已导入到 `env.py` 中 ### 8.3 数据库连接错误 **问题**:无法连接到数据库进行迁移 **解决方法**: 1. 检查数据库服务是否运行 2. 检查数据库连接字符串是否正确 3. 检查数据库用户权限是否正确 ### 8.4 迁移版本冲突 **问题**:多个开发者创建了相同版本号的迁移 **解决方法**: 1. 使用唯一的迁移版本号 2. 在提交迁移脚本前检查版本冲突 3. 如果发生冲突,重新生成迁移脚本 --- ## 9. 附录 ### 9.1 常用命令汇总 | 命令 | 说明 | 示例 | |------|------|------| | `alembic init` | 初始化 Alembic | `alembic init alembic` | | `alembic revision --autogenerate` | 自动生成迁移脚本 | `alembic revision --autogenerate -m "创建用户表"` | | `alembic revision` | 手动创建迁移脚本 | `alembic revision -m "添加用户状态字段"` | | `alembic upgrade head` | 应用所有迁移 | `alembic upgrade head` | | `alembic upgrade ` | 应用到特定版本 | `alembic upgrade 1234abcd` | | `alembic downgrade -1` | 回滚到上一个版本 | `alembic downgrade -1` | | `alembic downgrade ` | 回滚到特定版本 | `alembic downgrade 1234abcd` | | `alembic downgrade base` | 回滚到初始状态 | `alembic downgrade base` | | `alembic current` | 查看当前版本 | `alembic current` | | `alembic history` | 查看所有版本 | `alembic history` | | `alembic history --verbose` | 查看详细版本信息 | `alembic history --verbose` | | `alembic history --graph` | 查看迁移树 | `alembic history --graph` | ### 9.2 迁移最佳实践总结 1. **版本控制**: 将迁移脚本纳入版本控制 2. **测试**: 在应用到生产环境之前测试迁移 3. **备份**: 在应用迁移之前备份数据库 4. **简单**: 每个迁移只包含一个逻辑变更 5. **清晰**: 使用描述性的迁移消息 6. **完整**: 编写正确的升级和降级脚本 7. **协作**: 与团队成员协调迁移工作 8. **监控**: 监控迁移执行情况 ### 9.3 变更记录 | 版本 | 日期 | 修改人 | 修改内容 | |------|------|--------|----------| | V1.0 | 2026-01-26 | - | 初始版本创建 |