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

用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_apisys_role_menusys_user_role是通过ManyToManyField定义的多对多关系自动生成的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值