Search code examples
sqlalchemymigrationalembic

Alembic not autogenerating tables from SQLAlchemy classes


sqlalchemy: 2.0.25 alembic: 1.13.1 python: 3.11

alembic revision --autogenerate -m "initial migration" creates empty upgrade and downgrade code in the migration.

As shown below I have two model classes in separate files from the Base class.

My alembic/env.py is follows their docs instructions to import my Base class so that it can access metadata. On an empty db, this should produce upgrade code to create the Order and Item tables but it seems like it's not finding those classes from the Base.metadata. 1. On a hunch I put imports of those classes into the env.py (as shown below) and then the migration is autogenerated correctly. 2. As an alternate workaround I also found that putting all my models into one big file along with Base resulted in complete generation.

Workaround 1 is ok but kind of a hassle as the number of model classes grows. Workaround 2 is unacceptable.

Is there a more elegant and reliable way to get it find all model classes for autogeneration?

#models.base.py

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

#models.order.py

from models.base import Base

class Order(Base):
    __tablename__ = "Order"

    id: Mapped[int] = mapped_column(primary_key=True)
    description: Mapped[Optional[str]]
    items: Mapped[List["Item"]] = relationship(back_populates="order")
 
#models.item.py

from models.base import Base

class Item(Base):
    __tablename__ = "Item"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    description: Mapped[str] = mapped_column(String(255))

    order_id: Mapped[int] = mapped_column(ForeignKey("Order.id"))
    order: Mapped["Order"] = relationship(back_populates="items")
 
 

#alembic/env.py  (excerpt)
from models.base import Base
from models.order import Order # not prescribed by doc but makes it work
from models.item import Item # same as above
target_metadata = Base.metadata
 
 

Solution

  • You can define your Base class in an individual file e.g. base_class.py, reuse it in your models definition, e.g. order.py and import all your models into a base.py file to use in alembic env.py.

    E.g.

    # db.base_class.py
    
    from typing import Any
    from sqlalchemy.ext.declarative import as_declarative, declared_attr
    
    @as_declarative()
    class Base:
        id: Any
        __name__: str
        
        # Generate __tablename__ automatically
        @declared_attr
        def __tablename__(cls) -> str:
            return cls.__name__.lower()
    
    
    # models.order.py
    
    from db.base_class import Base
    
    class Order(Base):
        id: Mapped[int] = mapped_column(primary_key=True)
        description: Mapped[Optional[str]]
        items: Mapped[List["Item"]] = relationship(back_populates="order")
    
    
    # models.base.py
    
    from db.base_class import Base # noqa
    from models.order import Order # noqa
    from models.item import Item   # noqa
    
    

    If you have errors when creating the migration scripts, you can import the models in __init__.py of models

    # models.__init__.py
    
    from .order import Order # noqa
    from .item import Item   # noqa
    

    With this, you will not have to update alembic.env.py every time you create a model, but you will have to import it in models.base.py, as well as Order and Item.

    # alembic.env.py
    
    import sys
    from alembic import context
    from sqlalchemy import engine_from_config, pool
    from pathlib import Path
    
    sys.path.append(str(Path.cwd()))
    from db.base import Base
    
    
    config = context.config
    target_metadata = Base.metadata
    
    ...