[Python] Using generics to write an abstract ObjectRepository class

Discussão em 'Python' iniciado por Stack, Abril 14, 2025.

  1. Stack

    Stack Membro Participativo

    Edit: As noted in the comments by @PanagiotisKanavos, this method to interact with databases is redundant. Since the main question is about usage of Generics, I'll leave the question open.

    Original Text:

    I'm trying to write a generic class that acts as the Database Repository for a number of different SQLModel models.

    I have bound the _ModelType TypeVar to SQLModel hoping that it would remove all type warnings, but I still get warnings in when I return SQLModel types in methods that expect _ModelType. I also tried constraining the _ModelType to a SQLModel too, but it didn't make any difference.

    Here is the abstract repository definition.

    from sqlmodel import SQLModel
    import typing
    from sqlalchemy.engine.result import ScalarResult


    _ModelType = typing.TypeVar(
    "_ModelType",
    bound=SQLModel,
    )


    class BaseRepository(typing.Generic[_ModelType]):

    def __init__(self, session: Session, model_class: typing.Type[SQLModel]):
    self.session = session
    self.__model_class = model_class

    # Static hinting works as expected
    def save(self, obj: _ModelType) -> _ModelType:
    """
    Generic method to save an object to the database.
    """
    self.session.add(obj)
    return obj

    def get_one(self, obj_id: typing.Any) -> typing.Optional[_ModelType]:
    obj = self.session.get(self.__model_class, obj_id) #SQLModel | None
    return obj
    # Warning: Type "SQLModel" is not assignable to type "_ModelType@BaseRepository"

    def query(self,
    *conditions: typing.Union[_ColumnExpressionArgument[bool], bool]
    ) -> ScalarResult[_ModelType]:

    stmt = select(self.__model_class)
    for c in conditions:
    stmt = stmt.where(c)

    return self.session.exec(stmt).all()
    #Similar warning along with: Type parameter "_T_co@Sequence" is covariant,
    #but "SQLModel" is not a subtype of "_ModelType@BaseRepository"


    and here is a basic example of usage (The actual implementation wraps the session creation, management, all repositories creation, and auto-commit on exit in a UnitOfWork context manager).

    from utils.db_new import engine
    from order_management.repository_new import BaseRepository
    from order_management.models import Order, Customer
    from sqlmodel import Session
    import logging

    logging.basicConfig(level=logging.INFO)

    order_repo = BaseRepository[Order](Session(engine), Order)

    all_orders = order_repo.query(
    Order.pickup_type == "pickup",
    Customer.name == "Diaa Malek",
    )
    [print(o.id) for o in all_orders]

    order = order_repo.get_one(1088)
    if order:
    print(order.items)



    Functionality-wise, it works exactly as intended. Each return type is annotated correctly. I could also just type: ignore and move on with my life.

    But I'm still not sure I understand those warnings or how to avoid them. Am I misusing Generics in some way, or is this an anti-pattern or overcomplication that should not even exist?

    Also, is it possible to use _ModelType directly instead of passing the model_type in the Repository constructor? I realize it doesn't make any difference, but I wonder.

    References:

    Difference between TypeVar('T', A, B) and TypeVar('T', bound=Union[A, B])

    Repository Design Pattern

    Continue reading...

Compartilhe esta Página