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])
I think you would have less typing issues if model_class
was typed as typing.Type[_ModelType]
. (In the absence of a more self-contained example, it is difficult to see what the precise impact would be.)