如何用Alembic迁移基于DeclarativeBase的FastAPI Users User模型?
问题描述
- 作为FastAPI新手,学习FastAPI Users时遇到问题:已有用
sqlalchemy.Table定义的role模型,需要实现基于sqlalchemy.orm.DeclarativeBase的User模型的Alembic迁移,同时正确修改User类。 - 曾尝试改用
sqlalchemy.ext.declarative.declarative_base但无效果。
现有代码
auth/database.py
from datetime import datetime from typing import AsyncGenerator from fastapi import Depends from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase from config import DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy import Boolean, ForeignKey, Integer, String, TIMESTAMP, Column from models.models import role DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}" class Base(DeclarativeBase): pass class User(Base, SQLAlchemyBaseUserTable[int]): email: Mapped[str] = mapped_column( String(length=320), unique=True, index=True, nullable=False ) hashed_password: Mapped[str] = mapped_column( String(length=1024), nullable=False ) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) is_superuser: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False ) is_verified: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False ) id: Mapped[int] = mapped_column(Integer, primary_key=True) username: Mapped[str] = mapped_column(String, nullable=False) registered_at: Mapped[str] = mapped_column(TIMESTAMP, default=datetime.utcnow) role_id: Mapped[int] = mapped_column(Integer, ForeignKey(role.c.id)) engine = create_async_engine(DATABASE_URL) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) async def create_db_and_tables(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async with async_session_maker() as session: yield session async def get_user_db(session: AsyncSession = Depends(get_async_session)): yield SQLAlchemyUserDatabase(session, User)
models/models.py
from sqlalchemy import MetaData, Table, Column, Integer, String, JSON metadata = MetaData() role = Table( "role", metadata, Column("id", Integer, primary_key=True), Column("name", String, nullable=False), Column("permissions", JSON) )
migrations/env.py
from logging.config import fileConfig from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context from config import DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER # this is the Alembic Config object, which provides # access to the values within the .ini file in use. from models.models import metadata config = context.config section = config.config_ini_section config.set_section_option(section, "DB_HOST", DB_HOST) config.set_section_option(section, "DB_NAME", DB_NAME) config.set_section_option(section, "DB_PASS", DB_PASS) config.set_section_option(section, "DB_PORT", DB_PORT) config.set_section_option(section, "DB_USER", DB_USER) # 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 = 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()
尝试过的代码
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base # Declare a base from your metadata Base: DeclarativeMeta = declarative_base(metadata=metadata) # Create the User table with the class style class UserTable(Base, SQLAlchemyBaseUserTable): # Add your columns here name = Column(String(50))
解决方案
1. 统一元数据,让Base复用现有metadata
修改auth/database.py中的Base类,使其使用models.models中已定义的metadata,确保User表的元数据与role表合并:
from models.models import metadata # 新增导入 class Base(DeclarativeBase): metadata = metadata # 指定复用已有metadata
2. 简化User类定义,避免重复字段
SQLAlchemyBaseUserTable[int]已经内置了id、email、hashed_password、is_active、is_superuser、is_verified等字段,无需重复定义,只保留自定义字段即可,同时修正registered_at的类型错误:
class User(Base, SQLAlchemyBaseUserTable[int]): username: Mapped[str] = mapped_column(String, nullable=False) registered_at: Mapped[datetime] = mapped_column(TIMESTAMP, default=datetime.utcnow) # 类型改为datetime role_id: Mapped[int] = mapped_column(Integer, ForeignKey(role.c.id))
3. 调整Alembic配置,确保加载User模型
在migrations/env.py中导入User类,让SQLAlchemy将User表的元数据纳入到metadata中,Alembic才能识别到该表:
# 在文件顶部添加导入 from auth.database import User
4. 生成并执行迁移脚本
运行以下命令生成迁移脚本并应用:
alembic revision --autogenerate -m "add user table" alembic upgrade head
为什么之前的尝试无效?
之前使用declarative_base(metadata=metadata)创建Base时,没有正确结合SQLAlchemyBaseUserTable的字段定义,且未确保User模型被Alembic加载,导致元数据中没有User表的信息,无法生成对应的迁移脚本。
内容的提问来源于stack exchange,提问作者tommysyching
相关产品推荐
相关产品推荐

