☰
Tortoise ORM 内置迁移系统实战:基于 migrations_project 示例理解完整的迁移工作流
2026/10/12 3:25:53 网站建设 项目流程
  • 数据库
  • 后端

【免费下载链接】tortoise-orm

Familiar asyncio ORM for python, built with relations in mind

项目地址:https://gitcode.com/gh_mirrors/to/tortoise-orm
点击查看免费下载

导读

本文以 Tortoise ORM 仓库中的 examples/migrations_project 示例项目为主线,系统讲解 Tortoise ORM 内置迁移系统的完整工作流:从TORTOISE_ORM配置、init初始化迁移包,到makemigrations自动检测模型变更、migrate应用迁移,再到history/heads检查迁移状态,最后深入剖析迁移文件的内部结构、数据迁移(RunPython/RunSQL)以及底层执行原理。读完本文,你将能够在一个新项目中独立完成"配置 → 初始化 → 生成 → 应用 → 检查 → 回滚"的整套数据库版本管理操作,并能读懂仓库中任何一份自动生成的迁移文件。

说明:本文所述迁移系统是 Tortoise ORM 的内置方案,相关命令与模块均以当前仓库(tortoise/cli/cli.py、tortoise/migrations)的实际实现为准。

一、示例项目全景

examples/migrations_project是一个"启用迁移的最小示例项目"(A minimal project with migrations enabled),它的目录结构如下:

examples/migrations_project/ ├── README.rst # 项目说明与 CLI 操作指南(本文主线) ├── __init__.py ├── models.py # blog 应用的全部模型定义 ├── settings.py # TORTOISE_ORM 配置 └── migrations/ # 迁移包:14 个迁移文件 + __init__.py ├── 0001_initial.py ├── 0002_auto_20260124_1650.py ├── 0003_auto_20260124_1650.py ├── ... ├── 0013_add_post_summary.py └── 0014_rename_post_excerpt.py

这个项目麻雀虽小、五脏俱全:它定义了 6 个模型(Author、Post、Comment、State、Category、Tag),包含外键、多对多、source_field、unique约束、auto_now_add时间字段等典型场景,并且迁移目录中的 14 个文件完整记录了这些模型从无到有、再到逐步演进的全过程。

1.1 配置文件解析

examples/migrations_project/settings.py 是全项目唯一的配置入口:

TORTOISE_ORM = { "connections": { "default": "sqlite://db.sqlite3", }, "apps": { "blog": { "models": ["examples.migrations_project.models"], "default_connection": "default", "migrations": "examples.migrations_project.migrations", } }, }

各字段含义如下:

配置项示例值作用
connections.default"sqlite://db.sqlite3"默认数据库连接串,此处是 SQLite 文件数据库(相对当前工作目录生成db.sqlite3)
apps.blog.models["examples.migrations_project.models"]该应用加载的模型模块列表
apps.blog.default_connection"default"应用绑定的连接名
apps.blog.migrations"examples.migrations_project.migrations"迁移包路径,迁移系统工作的前提

其中migrations一项是内置迁移系统正常运行的关键:CLI 通过它找到磁盘上的迁移文件。如果在应用配置中省略该项,CLI 会尝试从models模块路径推断迁移包(例如把xxx.models推导为xxx.migrations),推断逻辑见 tortoise/cli/utils.py 中的infer_migrations_module();推断不到或导入失败时,会直接报错提示手动配置。

1.2 模型设计

examples/migrations_project/models.py 定义了一个小型博客数据模型:

  • Author:作者,IntField(pk=True)主键,full_name通过source_field="name"映射到底层列name;
  • Post:文章,含title(唯一字段slug)、body(source_field="content")、可空excerpt、auto_now_add的created_at,并通过ForeignKeyField关联作者、两个ManyToManyField关联分类与标签;
  • Comment:评论,外键关联Post;
  • State:状态,注意其Meta.table = "status",模型名与表名不一致;
  • Category/Tag:分类与标签。

这些模型里埋藏了大量值得迁移系统处理的场景:source_field重命名、unique约束、外键/多对多中间表、模型改名(Status→State)、字段改名(summary→excerpt)等,后面我们会逐一在迁移文件中看到它们对应的操作。

二、环境准备与第一个命令:init

所有迁移命令都要先解决配置来源问题。tortoiseCLI 支持三种配置方式(见 tortoise/cli/cli.py 的_load_config):

  1. -c/--config:形如module.VARIABLE的配置路径,例如settings.TORTOISE_ORM;
  2. --config-file:指向一个 JSON/YAML 配置文件;
  3. pyproject.toml中的[tool.tortoise]配置,或环境变量TORTOISE_ORM。

本示例使用第一种方式。第一步是在仓库根目录创建迁移包并生成初始迁移:

tortoise -c examples.migrations_project.settings.TORTOISE_ORM init tortoise -c examples.migrations_project.settings.TORTOISE_ORM makemigrations

2.1 init 做了什么

init命令的核心逻辑在 tortoise/cli/cli.py 与_ensure_migrations_package()(同文件 L211-L259):

  • 读取配置并解析出所有应用(_select_apps);
  • 对每个应用解析migrations模块路径;
  • 若迁移包目录不存在,则递归创建目录并写入空的__init__.py,使其成为可导入的 Python 包;
  • 输出形如blog: examples.migrations_project.migrations -> /path/to/examples/migrations_project/migrations的确认信息。

执行后,examples/migrations_project/migrations/__init__.py(空文件)就是init的产物。迁移包必须可导入,因为后续makemigrations、migrate都要通过importlib按模块路径加载其中的迁移模块(tortoise/migrations/loader.py)。

2.2 makemigrations 自动检测模型变更

makemigrations是迁移系统的核心命令,它会:

  1. 加载磁盘上已有的迁移,重放它们的操作构建"历史状态"(_project_state,见 tortoise/migrations/autodetector.py);
  2. 从当前运行的模型类构建"当前状态"(_current_state,同文件 L80-L87);
  3. 由OperationGenerator对比两个状态,生成AddField、CreateModel、RenameField等差异操作;
  4. 由MigrationWriter把操作序列化(deconstruct())成可重新导入、可回放的 Python 迁移文件。

在全新项目上首次执行,会生成0001_initial.py。仓库中该文件(examples/migrations_project/migrations/0001_initial.py)是手工整理的初始版本,展示了CreateModel操作的标准形态:

from tortoise import fields, migrations from tortoise.fields.base import OnDelete from tortoise.migrations import operations as ops class Migration(migrations.Migration): initial = True operations = [ ops.CreateModel( name="Author", fields=[ ( "id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True), ), ("name", fields.CharField(max_length=200)), ], options={"table": "author", "app": "blog", "pk_attr": "id"}, bases=["Model"], ), ops.CreateModel( name="Post", fields=[ ... ( "author", fields.ForeignKeyField( "blog.Author", source_field="author_id", db_constraint=True, to_field="id", related_name="posts", on_delete=OnDelete.CASCADE, ), ), ], options={"table": "post", "app": "blog", "pk_attr": "id"}, bases=["Model"], ), ]

注意几个细节:

  • 迁移类继承tortoise.migrations.migration.Migration,声明initial = True表示这是应用的初始迁移;
  • CreateModel的options中table是实际表名、app是应用标签、pk_attr是主键字段名;
  • 外键被展开为底层列(source_field="author_id"),并携带to_field、related_name、on_delete等完整关系信息;
  • 每个字段的generated=True表示自增主键,db_index=True等属性与模型定义一一对应。

三、应用迁移:migrate

生成迁移文件后,需要把它们真正应用到数据库:

tortoise -c examples.migrations_project.settings.TORTOISE_ORM migrate

migrate的执行流程(tortoise/cli/cli.py → tortoise/migrations/api/migrate.py):

  1. 初始化 Tortoise(不建立业务连接,init_connections=False);
  2. 按default_connection把应用分组到各连接;
  3. 为每个连接创建MigrationExecutor(tortoise/migrations/executor.py),由它读取迁移图、对比已应用记录、生成执行计划;
  4. 依次对每个Migration调用apply()(tortoise/migrations/migration.py):先推进内存状态(state_forward),再通过schema_editor执行对应 DDL;
  5. 执行完毕后由MigrationRecorder把迁移记录写入数据库中的tortoise_migrations表(模型定义见 tortoise/migrations/recorder.py,包含app、name、applied_at字段,(app, name)唯一约束)。

执行时终端会输出类似下面的进度信息:

Applying blog.0001_initial... OK Applying blog.0002_auto_20260124_1650... OK

migrate还支持指定目标:

tortoise -c examples.migrations_project.settings.TORTOISE_ORM migrate blog # 只迁移 blog 应用到最新 tortoise -c examples.migrations_project.settings.TORTOISE_ORM migrate blog 0002_auto_20260124_1650 # 迁移到指定版本

其中upgrade是migrate的别名(方向固定为forward),另外migrate支持--fake(只记录不执行 SQL)和--dry-run(只预览不落库)两个开关。

四、检查迁移状态:history 与 heads

应用迁移后,可以通过两个命令从不同视角检查状态:

tortoise -c examples.migrations_project.settings.TORTOISE_ORM history tortoise -c examples.migrations_project.settings.TORTOISE_ORM heads
  • history:从数据库读取已应用记录(tortoise/cli/cli.py),按连接、按应用分组打印,例如- blog 0001_initial、- blog 0002_auto_20260124_1650;若某应用尚未应用任何迁移则提示(no applied migrations);
  • heads:只读磁盘上的迁移文件,计算迁移图中的"叶节点"(即没有后继迁移的最新版本,同文件 L615-L625),不会访问数据库。注意它通过_NoopRecorder构造加载器,因此无需数据库连接即可工作。

两者对比正是排查问题的利器:history与heads不一致,说明磁盘上有新迁移尚未应用(或数据库记录了已不存在的迁移),此时执行migrate即可补齐。

五、通过 Python 调用 CLI

如果命令行环境不便,还可以用python3 -m tortoise以模块方式调用同一套 CLI:

python3 -m tortoise -c examples.migrations_project.settings.TORTOISE_ORM migrate

这与tortoise命令完全等价,因为 tortoise/main.py 与 tortoise/cli/cli.py 的main()都最终调用run_cli_async()。main()中还会把当前目录插入sys.path(sys.path.insert(0, ".")),确保从仓库根目录运行即可解析到examples.migrations_project包。

六、迁移文件的演进图谱:14 个迁移背后的操作类型

examples/migrations_project/migrations/目录下的 14 个文件构成了一条完整、可回放的迁移链。下表按顺序归纳了每个文件的操作(括号内为对应的 Tortoise 迁移操作类):

迁移文件核心操作说明
0001_initial.pyCreateModel× 2初始创建Author、Post(含外键author)
0002_auto_20260124_1650.pyAddField给Post增加唯一字段slug
0003_auto_20260124_1650.pyAddField给Post增加可空published_at时间字段
0004_auto_20260124_1650.pyCreateModel创建Comment(外键指向Post)
0005_auto_20260124_1650.pyRenameField作者字段name→full_name
0006_auto_20260124_1650.pyAlterField+RenameField调整full_name的source_field;Post.content→body
0007_auto_20260124_1650.pyAlterField× 2body恢复source_field="content",title加长到 300
0008_auto_20260124_1650.pyCreateModel+AddField创建Category,为Post添加多对多categories(中间表post_category)
0009_auto_20260124_1651.pyRemoveField删除Post.published_at
0010_auto_20260124_1651.pyCreateModel+AddField创建Tag,为Post添加多对多tags(中间表post_tag)
0011_auto_20260124_1651.pyCreateModel创建Status(表名status)
0012_auto_20260124_1651.pyRenameModelStatus→State
0013_add_post_summary.pyAddField+RunPython新增可空summary字段,并回填数据
0014_rename_post_excerpt.pyRenameFieldsummary→excerpt

这 14 个文件几乎覆盖了内置迁移系统的全部 schema 操作类型:CreateModel/DeleteModel/RenameModel、AddField/RemoveField/RenameField/AlterField、以及约束与索引相关操作。每一个文件都通过dependencies声明前驱迁移,例如0014_rename_post_excerpt.py中dependencies = [("blog", "0013_add_post_summary")],从而在磁盘上构成一张有序的依赖图(tortoise/migrations/graph.py 负责构建与校验)。

6.1 数据迁移:RunPython 实战

真正的生产项目里,单纯改表结构往往不够——新字段往往需要回填数据。0013_add_post_summary.py(examples/migrations_project/migrations/0013_add_post_summary.py)展示了标准的"加字段 + 数据迁移"组合:

from tortoise import fields, migrations from tortoise.expressions import F from tortoise.migrations import operations as ops async def populate_post_summary(apps, schema_editor) -> None: Post = apps.get_model("blog.Post") await Post.filter(summary=None).update(summary=F("title")) async def reset_post_summary(apps, schema_editor) -> None: Post = apps.get_model("blog.Post") await Post.all().update(summary=None) class Migration(migrations.Migration): dependencies = [("blog", "0012_auto_20260124_1651")] initial = False operations = [ ops.AddField( model_name="Post", name="summary", field=fields.TextField(null=True, unique=False), ), ops.RunPython( code=populate_post_summary, reverse_code=reset_post_summary, ), ]

关键点:

  • RunPython的两个回调都是async def,签名固定为(apps, schema_editor);
  • 回调中不能直接 import 运行时模型类,必须通过apps.get_model("blog.Post")获取历史模型——迁移系统会按该迁移执行时的 schema 重建模型(StateApps,见 tortoise/migrations/schema_generator/state_apps.py),保证查询与当时的表结构一致;
  • 这里还演示了F表达式:Post.filter(summary=None).update(summary=F("title"))用 SQL 层面把title的值回填到summary,避免把数据拉回 Python 再逐条写入;
  • reverse_code提供反向逻辑,使迁移可回滚。

RunPython的底层实现(tortoise/migrations/operations.py)规定:必须传入可调用对象,reverse_code若为None则该操作不可逆(reversible = reverse_code is not None),此时执行downgrade会抛出ValueError。

6.2 字段重命名

0014_rename_post_excerpt.py(examples/migrations_project/migrations/0014_rename_post_excerpt.py)只做了一个操作:

ops.RenameField( model_name="Post", old_name="summary", new_name="excerpt", )

字段改名不涉及数据搬运(同一列仅改逻辑名),因此无需RunPython。这也解释了迁移文件的设计哲学:schema 操作与数据操作分离,各自使用最合适的操作类型。RenameField同样通过deconstruct()序列化,可被加载器重新导入并执行(tortoise/migrations/writer.py 负责生成这类可回放的 Python 模块)。

七、数据迁移的另一个选择:RunSQL

除RunPython外,内置迁移系统还提供RunSQL直接执行原始 SQL(定义见 tortoise/migrations/operations.py):

from tortoise.migrations import RunSQL from tortoise.migrations.migration import Migration class Migration(Migration): dependencies = [("blog", "0001_initial")] operations = [ RunSQL( sql="UPDATE post SET title = 'Migrated' WHERE title IS NULL", reverse_sql="UPDATE post SET title = NULL WHERE title = 'Migrated'", ), ]

RunSQL还支持参数化查询与多条语句:

RunSQL( sql=[ ("INSERT INTO post (title) VALUES (?)", ["First"]), ("INSERT INTO post (title) VALUES (?)", ["Second"]), ], reverse_sql="DELETE FROM post WHERE title IN ('First', 'Second')", )

选择建议:

  • 逻辑复杂、涉及条件判断或多模型联动、需要跨数据库移植时,用RunPython(享受 ORM 的类型安全与查询抽象);
  • 只是简单 UPDATE / INSERT / DELETE、追求大批量性能、或必须使用数据库专有特性时,用RunSQL;
  • RunPython与RunSQL可混用在同一个迁移的operations列表里,例如先用RunPython计算汇总值,再用RunSQL拼接字符串列。

此外两者都支持atomic参数(默认True)控制事务包裹:SQLite 的RunSQL建议设atomic=False以防连接死锁;PostgreSQL 的CREATE INDEX CONCURRENTLY不能在事务内执行,也需要关闭。对应实现见 tortoise/migrations/migration.py 中_run_database_forward/_backward对atomic_operation的判断。

八、从 CLI 命令到源码:迁移系统的运行时架构

把上面的命令串起来,可以得到内置迁移系统的完整运行时链路:

tortoise CLI (tortoise/cli/cli.py) ├── config 解析:_load_config (cli.py#L171) ├── 磁盘迁移加载:MigrationLoader.load_disk (tortoise/migrations/loader.py#L33) ├── 依赖图构建:MigrationGraph (tortoise/migrations/graph.py) ├── 已应用记录:MigrationRecorder (tortoise/migrations/recorder.py) ├── 执行引擎:MigrationExecutor (tortoise/migrations/executor.py) ├── 迁移对象:Migration.apply/unapply (tortoise/migrations/migration.py) ├── 操作定义:Operation (tortoise/migrations/operations.py) └── DDL 生成:各后端 schema_editor (tortoise/migrations/schema_editor/)

各模块职责(与官方 docs/migration.rst 的模块概览一致):

层级模块职责
公共入口tortoise.migrations.api.migrate以编程方式应用迁移
公共入口tortoise.migrations.api.plan构建 dry-run 计划而不执行 SQL
公共入口tortoise.migrations重新导出RunPython、RunSQL、CreateModel等供编写迁移使用
运行时tortoise.migrations.executor迁移规划与执行引擎
运行时tortoise.migrations.loader从磁盘加载迁移模块
运行时tortoise.migrations.graph迁移依赖图
运行时tortoise.migrations.recorder读写已应用迁移记录
运行时tortoise.migrations.migrationMigration基类与 apply/unapply 流程
Schema/状态tortoise.migrations.schema_generator.state内存中的历史状态
Schema/状态tortoise.migrations.schema_generator.state_apps历史模型的应用注册表
Schema/状态tortoise.migrations.schema_editor各后端 DDL 编辑器
Schema/状态tortoise.migrations.operationsschema 变更操作定义
自动检测tortoise.migrations.autodetector对比当前应用与迁移状态,生成新操作
自动检测tortoise.migrations.writer把操作渲染为迁移模块文件

迁移的执行以"状态重放"为灵魂:每个操作都同时具备state_forward(推进内存状态)与database_forward(落库执行)两套行为,因此同一份迁移既可以 dry-run 预演、又可以生成 SQL 预览,还可以真正执行——这也是sqlmigrate、--dry-run等能力的基础。

九、完整 CLI 参考与常见问题

9.1 CLI 命令速查

除本文前面已演示的命令外,内置 CLI 还提供以下命令(均支持-c/--config-file配置解析,完整实现见 tortoise/cli/cli.py):

# 回滚(卸载)迁移:downgrade 需要指定 APP_LABEL tortoise -c examples.migrations_project.settings.TORTOISE_ORM downgrade blog tortoise -c examples.migrations_project.settings.TORTOISE_ORM downgrade blog 0001_initial # 预览迁移的 SQL,而不执行 tortoise -c examples.migrations_project.settings.TORTOISE_ORM sqlmigrate blog 0001_initial tortoise -c examples.migrations_project.settings.TORTOISE_ORM sqlmigrate blog 0001_initial --backward # 创建空迁移(用于手工编写 RunPython/RunSQL) tortoise -c examples.migrations_project.settings.TORTOISE_ORM makemigrations blog --empty tortoise -c examples.migrations_project.settings.TORTOISE_ORM makemigrations -n add_posts_table # 交互式 Shell(需安装 ipython 或 ptpython) tortoise -c examples.migrations_project.settings.TORTOISE_ORM shell

命令细节说明:

  • downgrade不指定迁移名时回滚到该应用的最早状态(__first__),未指定APP_LABEL时会报错并列出可用应用标签(tortoise/cli/cli.py);
  • sqlmigrate对 PostgreSQL 系连接会自动包裹BEGIN;/COMMIT;,便于直接粘贴执行(同文件 L628-L684);
  • migrate/upgrade/downgrade均支持--fake与--dry-run;
  • makemigrations --name会把自动命名(auto_20260124_1650)替换为0002_add_posts_table这样的可读名称(格式化逻辑format_migration_name见 tortoise/migrations/writer.py)。

9.2 常见错误排查

针对迁移系统最常见的报错,官方文档 docs/migration.rst 给出了明确的排查思路:

症状原因与解法
"Migrations are not found"应用配置缺少migrations模块路径,或迁移包不存在(执行tortoise init创建)
App <label> has no migrations configured在应用配置中补上"migrations": "myapp.migrations"后重试
No module named <app>.migrations迁移包未创建或不在PYTHONPATH上,确认包可导入
CLI 提示 "No changes detected"模型未被配置中的应用导入,或init与当前命令使用了不同配置源(如-c与--config-file混用)
数据迁移导入模型失败RunPython回调应通过apps.get_model获取历史模型,而不是直接 import 运行时模型类
迁移不可回滚破坏性数据操作应设置reverse_code=None(RunPython)或省略reverse_sql(RunSQL),此时执行downgrade会报错,这是预期行为

十、结语

从examples/migrations_project这个 40 行的示例开始,我们走完了 Tortoise ORM 内置迁移系统的完整旅程:init创建迁移包 →makemigrations自动检测模型差异 →migrate应用变更 →history/heads双视角核查状态 →downgrade回滚 →sqlmigrate预览 SQL。14 个迁移文件展示了CreateModel、AddField、AlterField、RenameField、RenameModel、RemoveField、RunPython等全部核心操作类型,而源码层面的加载器、依赖图、状态重放与执行器则解释了这些命令为何能够做到"可预测、可回放、可回滚"。

对任何准备在 asyncio 项目中使用 Tortoise ORM 的团队来说,这套内置迁移系统就是数据库 schema 的版本控制工具——推荐直接以examples/migrations_project为模板起步,把迁移纳入日常开发流程,让每次模型变更都有据可查、可逆可回。

  • 数据库
  • 后端

【免费下载链接】tortoise-orm

Familiar asyncio ORM for python, built with relations in mind

项目地址:https://gitcode.com/gh_mirrors/to/tortoise-orm
点击查看免费下载
上一篇:LeagueAkari:英雄联盟玩家的智能效率革命,告别传统低效操作
下一篇:3大核心突破:Visual Syslog Server如何解决Windows环境下的企业日志管理难题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询