Files
xsl_node/backend/docs/后端数据库迁移文档.md
T
2026-01-26 12:29:56 +08:00

790 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端数据库迁移文档
## 文档信息
- **文档版本**: 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 <revision_id>
# 示例
alembic upgrade 1234abcd
```
### 4.3 回滚迁移
#### 回滚到上一个版本
使用以下命令回滚到上一个迁移版本:
```bash
# 在项目根目录执行
alembic downgrade -1
```
#### 回滚到特定版本
使用以下命令回滚到特定迁移版本:
```bash
# 在项目根目录执行
alembic downgrade <revision_id>
# 示例
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 <revision>` | 应用到特定版本 | `alembic upgrade 1234abcd` |
| `alembic downgrade -1` | 回滚到上一个版本 | `alembic downgrade -1` |
| `alembic downgrade <revision>` | 回滚到特定版本 | `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 | - | 初始版本创建 |