save code

This commit is contained in:
Your Name
2026-01-26 12:29:56 +08:00
parent 4408f41d81
commit 40d2f3f6ac
13 changed files with 4891 additions and 124 deletions
+789
View File
@@ -0,0 +1,789 @@
# 后端数据库迁移文档
## 文档信息
- **文档版本**: 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 | - | 初始版本创建 |