用Tortoise-ORM +Aerich实现数据库迁移自动化
安装必要依赖
pip install tortoise-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文件,代码如下:
from tortoise import fields, models
class BaseModel(models.Model):
id = fields.BigIntField(pk=True, index=True)
class Meta:
abstract = True
class OperatorMixin:
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文件,代码如下:
from tortoise import fields
from .base import BaseModel, OperatorMixin
from schemas.menus import MenuType
from .enums import MethodType
class User(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'
)
class Meta:
table = "sys_user"
table_description = "用户表"
class Dept(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-未删除)')
class Meta:
table = "sys_dept"
table_description = "部门表"
class Role(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-已删除)')
class Meta:
table = "sys_role"
table_description = "角色表"
class Menu(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)
class Meta:
table = "sys_menu"
table_description = "菜单表"
class Api(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)
class Meta:
table = "sys_api"
创建models/enums.py文件,代码如下:
from enum import StrEnum
class MethodType(StrEnum):
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
PATCH = "PATCH"
创建schemas/menus.py文件,代码如下:
from enum import StrEnum
class MenuType(StrEnum):
CATALOG = "catalog" # 目录
MENU = "menu" # 菜单
BUTTON = 'button', # 按钮
EXTLINK = 'extlink', # 外链
创建models/__init__.py文件,代码如下:
from .admin import *
代码解析
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文件,添加如下代码:
# SQLite fallback configuration
TORTOISE_ORM = {
"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文件,代码如下:
from fastapi import FastAPI
from api import api_router
from aerich import Command
from log import logger
from settings.config import settings
def register_routers(app: FastAPI, prefix: str = "/api"):
app.include_router(api_router, prefix=prefix)
async def init_db():
command = Command(tortoise_config=settings.TORTOISE_ORM)
try:
await command.init_db(safe=True)
except FileExistsError:
pass
await command.init()
try:
await command.migrate(no_input=True)
except AttributeError as e:
logger.error(f"数据库迁移失败: {e}")
logger.warning("请手动检查数据库和migrations状态")
raise RuntimeError("数据库迁移失败,请检查数据库连接和migrations状态") from e
await command.upgrade(run_in_transaction=True)
async def init_data():
logger.info("🚀 系统初始化开始...")
logger.info("🔧 开始数据库初始化和迁移...")
await init_db()
logger.info("✅ 数据库初始化完成")
修改main.py文件,代码如下:
from fastapi import FastAPI
from contextlib import asynccontextmanager
from tortoise import Tortoise
from core.init_app import register_routers, init_data
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_data()
yield
await Tortoise.close_connections()
app = FastAPI(lifespan=lifespan)
register_routers(app, prefix="/api")
执行uvicorn main:app运行项目

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

其中,sys_role_api、sys_role_menu和sys_user_role是通过ManyToManyField定义的多对多关系自动生成的。
数据库迁移&spm=1001.2101.3001.5002&articleId=161613360&d=1&t=3&u=da02d6b41447419a889207f6ba6f9f52)
328

被折叠的 条评论
为什么被折叠?



