企业级FastAPI后端模板搭建(四)数据库迁移

📅 2026/7/5 15:40:21 👁️ 阅读次数 📝 编程学习
企业级FastAPI后端模板搭建(四)数据库迁移

用Tortoise-ORM +Aerich实现数据库迁移自动化

安装必要依赖

pipinstalltortoise-orm aerich

补充说明:安装数据库驱动 pip install tortoise-orm[asyncodbc];Tortoise 当前支持以下数据库:SQLite (using aiosqlite)、PostgreSQL >= 9.4 (using asyncpg)、MySQL/MariaDB (using asyncmy)、Microsoft SQL Server/Oracle (using asyncodbc)

数据模型定义

创建models/base.py文件,代码如下:

fromtortoiseimportfields,modelsclassBaseModel(models.Model):id=fields.BigIntField(pk=True,index=True)classMeta:abstract=TrueclassOperatorMixin:create_by=fields.BigIntField(index=True,description='创建人ID',null=True)create_time=fields.DatetimeField(auto_now_add=True,index=True,null=True,description='创建时间')update_by=fields.BigIntField(index=True,description='修改人ID',null=True)update_time=fields.DatetimeField(auto_now=True,index=True,null=True,description='更新时间')

创建models/admin.py文件,代码如下:

fromtortoiseimportfieldsfrom.baseimportBaseModel,OperatorMixinfromschemas.menusimportMenuTypefrom.enumsimportMethodTypeclassUser(BaseModel,OperatorMixin):account=fields.CharField(description='账号',max_length=64,unique=True)username=fields.CharField(description='用户名称',max_length=64,null=True)gender=fields.SmallIntField(default=1,description='性别(1-男, 2-女, 0-保密)')password=fields.CharField(max_length=128,description='密码')avatar=fields.CharField(index=True,description='用户头像',max_length=255,null=True)mobile=fields.CharField(index=True,description='联系方式',max_length=20,null=True)email=fields.CharField(index=True,max_length=255,description='邮箱')status=fields.SmallIntField(default=1,description='状态(1-正常, 0-禁用)')is_superuser=fields.BooleanField(default=False,description='是否为超级管理员')last_login=fields.DatetimeField(null=True,description="最后登录时间",index=True)is_deleted=fields.SmallIntField(default=0,description='逻辑删除标识(0-未删除, 1-已删除)')dept=fields.ForeignKeyField('models.Dept',related_name="users",index=True,null=True,on_delete=fields.SET_NULL,description='部门ID')roles=fields.ManyToManyField("models.Role",related_name="users",through="sys_user_role",forward_key='user_id',backward_key='role_id')classMeta:table="sys_user"table_description="用户表"classDept(BaseModel,OperatorMixin):name=fields.CharField(max_length=100,unique=True,description="部门名称",index=True)code=fields.CharField(max_length=100,unique=True,description="部门编号",index=True)parent_id=fields.IntField(default=0,description="父节点ID",index=True)tree_path=fields.CharField(max_length=255,description="父节点ID路径",index=True)sort=fields.SmallIntField(default=0,description="显示顺序")status=fields.SmallIntField(default=1,description="状态(1-正常 0-禁用)")is_deleted=fields.SmallIntField(default=0,description='逻辑删除标识(1-已删除 0-未删除)')classMeta:table="sys_dept"table_description="部门表"classRole(BaseModel,OperatorMixin):name=fields.CharField(description='角色名称',max_length=64,unique=True)code=fields.CharField(description='角色编码',max_length=32,unique=True)sort=fields.IntField(description='显示顺序',index=True,null=True)status=fields.SmallIntField(default=1,description='角色状态(1-正常, 0-停用)')data_scope=fields.SmallIntField(index=True,description='数据权限(1-所有数据, 2-部门及子部门, 3-本部门, 4-本人)',null=True)desc=fields.CharField(max_length=500,null=True,description="角色描述")# 多对多关系menus=fields.ManyToManyField("models.Menu",related_name="roles",through="sys_role_menu",forward_key='role_id',backward_key='menu_id')apis=fields.ManyToManyField("models.Api",related_name="apis",through="sys_role_api",forward_key='role_id',backward_key='api_id')is_deleted=fields.SmallIntField(default=0,description='逻辑删除标识(0-未删除, 1-已删除)')classMeta:table="sys_role"table_description="角色表"classMenu(BaseModel,OperatorMixin):parent_id=fields.BigIntField(description='父菜单ID')tree_path=fields.CharField(max_length=255,description='父节点ID路径',null=True,index=True)title=fields.CharField(max_length=64,description='菜单名称')type=fields.CharEnumField(MenuType,null=True,description="菜单类型(C-目录 M-菜单 B-按钮)")name=fields.CharField(max_length=255,description='路由名称(Vue Router 中用于命名路由)',null=True,index=True)path=fields.CharField(max_length=128,description='路由路径(Vue Router 中定义的 URL 路径)',null=True,index=True)component=fields.CharField(max_length=128,description='组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)',null=True,index=True)perm=fields.CharField(max_length=128,description='权限标识(按钮专用)',null=True,index=True)always_show=fields.SmallIntField(default=0,description='【目录】仅一个子路由时是否始终显示(1-是, 0-否)')keep_alive=fields.SmallIntField(default=0,description='【菜单】是否开启页面缓存(1-是, 0-否)')visible=fields.SmallIntField(default=1,description='显示状态(1-显示, 0-隐藏)')sort=fields.IntField(default=0,description='排序')icon=fields.CharField(max_length=64,description='菜单图标',null=True,index=True)redirect=fields.CharField(max_length=128,description='跳转路径',null=True,index=True)params=fields.CharField(max_length=255,description='路由参数',null=True,index=True)classMeta:table="sys_menu"table_description="菜单表"classApi(BaseModel,OperatorMixin):path=fields.CharField(max_length=100,description="API路径",index=True)method=fields.CharEnumField(MethodType,description="请求方法",index=True)summary=fields.CharField(max_length=500,description="请求简介",index=True)tags=fields.CharField(max_length=100,description="API标签",index=True)classMeta:table="sys_api"

创建models/enums.py文件,代码如下:

fromenumimportStrEnumclassMethodType(StrEnum):GET="GET"POST="POST"PUT="PUT"DELETE="DELETE"PATCH="PATCH"

创建schemas/menus.py文件,代码如下:

fromenumimportStrEnumclassMenuType(StrEnum):CATALOG="catalog"# 目录MENU="menu"# 菜单BUTTON='button',# 按钮EXTLINK='extlink',# 外链

创建models/__init__.py文件,代码如下:

from.adminimport*

代码解析

ForeignKeyField:定义外键

dept=fields.ForeignKeyField('models.Dept',related_name="users",index=True,null=True,on_delete=fields.SET_NULL,description='部门ID')
  • model_name‌:必填项,写明要关联的模型名称,支持字符串形式以便延迟加载 。
  • related_name‌:用于在关联模型上创建反向关系,方便从另一方查询当前模型的数据 。
  • on_delete‌:定义当被关联的数据被删除时的处理策略,默认是级联删除 。
    • ‌CASCADE‌:级联删除,关联模型被删,当前模型对应数据也跟着删 。
    • SET_NULL‌:置为空,关联模型被删,外键字段变为 NULL,需设置 null=True 。
    • RESTRICT‌:限制删除,只要有外键指向,就不允许删除关联模型 。
    • SET_DEFAULT‌:重置为默认值,关联模型被删,外键字段变为默认值,需设置 default 。‌‌‌‌
  • db_constraint‌:控制是否在数据库层面创建外键约束,默认开启以保证数据完整性 。‌‌‌

ManyToManyField:定义两个模型之间的多对多关系

roles=fields.ManyToManyField("models.Role",related_name="users",through="sys_user_role",forward_key='user_id',backward_key='role_id')
  • related_model(位置参数)‌:目标模型的字符串路径(如 “models.Role”),必须指定。
  • related_name‌:反向关系名,用于从目标模型访问此关系(如 related_name=“users”)。
  • through‌:自定义中间表名(字符串,如 “sys_user_role”);不指定时自动创建。
  • forward_key:指定中间表里指向“定义该字段的模型”的外键列名,默认自动生成为 {model_name}_id(小写)(例如,若模型是 User,则默认 forward_key=“user_id”)。
  • backward_key: 对应中间表中指向“目标模型”的外键列,默认为 {target_model_name}_id。

**CharEnumField **:用于数据库字段的字符串枚举类型

type=fields.CharEnumField(MenuType,null=True,description="菜单类型(C-目录 M-菜单 B-按钮)")
  • enum_type:枚举类
  • description:描述。若未指定,将自动设置为包含“名称: 值”对的多行列表。
  • max_length:长度。若为零,则会从enum_type自动检测。

注册模型

修改settings/config.py文件,添加如下代码:

@propertydefTORTOISE_ORM(self)->dict:# SQLite fallback configurationreturn{"connections":{"default":{"engine":"tortoise.backends.sqlite","credentials":{"file_path":"fastapi_backend.sqlite3"},}},"apps":{"models":{"models":["models","aerich.models"],"default_connection":"default",},},"use_tz":False,"timezone":"Asia/Shanghai",}

修改core/init_app.py文件,代码如下:

fromfastapiimportFastAPIfromapiimportapi_routerfromaerichimportCommandfromlogimportloggerfromsettings.configimportsettingsdefregister_routers(app:FastAPI,prefix:str="/api"):app.include_router(api_router,prefix=prefix)asyncdefinit_db():command=Command(tortoise_config=settings.TORTOISE_ORM)try:awaitcommand.init_db(safe=True)exceptFileExistsError:passawaitcommand.init()try:awaitcommand.migrate(no_input=True)exceptAttributeErrorase:logger.error(f"数据库迁移失败:{e}")logger.warning("请手动检查数据库和migrations状态")raiseRuntimeError("数据库迁移失败,请检查数据库连接和migrations状态")fromeawaitcommand.upgrade(run_in_transaction=True)asyncdefinit_data():logger.info("🚀 系统初始化开始...")logger.info("🔧 开始数据库初始化和迁移...")awaitinit_db()logger.info("✅ 数据库初始化完成")

修改main.py文件,代码如下:

fromfastapiimportFastAPIfromcontextlibimportasynccontextmanagerfromtortoiseimportTortoisefromcore.init_appimportregister_routers,init_data@asynccontextmanagerasyncdeflifespan(app:FastAPI):awaitinit_data()yieldawaitTortoise.close_connections()app=FastAPI(lifespan=lifespan)register_routers(app,prefix="/api")

执行uvicorn main:app运行项目

项目启动之后会自动生成迁移文件(文件在migrations/目录下面) 和 数据库文件(fastapi_backend.sqlite3),连接数据库之后可以看到如下图所示的表格:


其中,sys_role_apisys_role_menusys_user_role是通过ManyToManyField定义的多对多关系自动生成的。