我一直在使用FastAPI/SQLAlChemy开发我的第一个API.对于数据库中的多个不同实体,我一直使用相同的四种方法(Get One、Get All、Post、Delete),因此创建了大量重复的代码.例如,下面的代码显示了真菌实体的方法.

from typing import List, TYPE_CHECKING

if TYPE_CHECKING: from sqlalchemy.orm import Session

import models.fungus as models
import schemas.fungus as schemas

async def create_fungus(fungus: schemas.CreateFungus, db: "Session") -> schemas.Fungus:
    fungus = models.Fungus(**fungus.dict())
    db.add(fungus)
    db.commit()
    db.refresh(fungus)
    return schemas.Fungus.from_orm(fungus)

async def get_all_fungi(db: "Session") -> List[schemas.Fungus]:
    fungi = db.query(models.Fungus).limit(25).all()
    return [schemas.Fungus.from_orm(fungus) for fungus in fungi]


async def get_fungus(fungus_id: str, db: "Session") -> schemas.Fungus:
    fungus = db.query(models.Fungus).filter(models.Fungus.internal_id == fungus_id).first()
    return fungus


async def delete_fungus(fungus_id: str, db: "Session") -> int:
    num_rows = db.query(models.Fungus).filter_by(id=fungus_id).delete()
    db.commit()
    return num_rows

我一直在try 用一个接口类来实现这四个独立于实体的方法的抽象设计模式.

然而,据我所知,新的Python标准和FastAPI要求对Python进行类型化.那么,我应该如何输入这些函数的返回类型,而不是schemas.Fungus,或者参数schemas.CreateFungusmodels.Fungus呢?

我认为我可以使用这些值的类型,它们是<class 'pydantic.main.ModelMetaclass'><class 'sqlalchemy.orm.decl_api.DeclarativeMeta'>.然而,我不确定这是正确的还是鼓励的.

推荐答案

我不知道你为什么要走abstract路.但对于这些常见的CRUD操作,generic接口当然是可能的.

您可以编写您自己的generic class,它(例如)被参数化为

  1. 用于数据库事务的ORM模型(来自SQLAlChemy),
  2. 获取/列出对象之类的东西的"响应"模型(来自Pydatics),以及
  3. 用于添加新数据的"创建"模型(来自Pydtic).

下面是这样一个类的示例,它基于您对真菌模型所做的操作:

from typing import Generic, TypeVar

from pydantic import BaseModel
from sqlalchemy.orm import DeclarativeMeta, Session

GetModel = TypeVar("GetModel", bound=BaseModel)
CreateModel = TypeVar("CreateModel", bound=BaseModel)
ORM = TypeVar("ORM", bound=DeclarativeMeta)


class CRUDInterface(Generic[ORM, GetModel, CreateModel]):
    orm_model: ORM
    get_model: type[GetModel]
    create_model: type[CreateModel]

    def __init__(
        self,
        orm_model: ORM,
        get_model: type[GetModel],
        create_model: type[CreateModel],
        db_id_field: str = "id",
    ) -> None:
        self.orm_model = orm_model
        self.get_model = get_model
        self.create_model = create_model
        self.db_id_field = db_id_field

    def create(self, data: CreateModel, db: Session) -> GetModel:
        new_instance = self.orm_model(**data.dict())
        db.add(new_instance)
        db.commit()
        db.refresh(new_instance)
        return self.get_model.from_orm(new_instance)

    def list(self, db: Session, limit: int = 25) -> list[GetModel]:
        objects = db.query(self.orm_model).limit(limit).all()
        return [self.get_model.from_orm(obj) for obj in objects]

    def get(self, id_: int, db: Session) -> GetModel:
        where = getattr(self.orm_model, self.db_id_field) == id_
        obj = db.query(self.orm_model).filter(where).first()
        return self.get_model.from_orm(obj)

    def delete(self, id_: int, db: Session) -> int:
        filter_kwargs = {self.db_id_field: id_}
        num_rows = db.query(self.orm_model).filter_by(**filter_kwargs).delete()
        db.commit()
        return num_rows

现在,就您在示例中显示的CRUD操作所涉及的模型而言,这个类是完全泛型的,这意味着在实例化时,将在其方法中正确键入一个CRUDInterface的对象,并且不需要在初始化之后传递specific个模型.模型只是作为实例属性保存.

现在假设您有以下模型:(简化)

from pydantic import BaseModel
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeMeta, declarative_base


ORMBase: DeclarativeMeta = declarative_base()


class Fungus(BaseModel):
    id: int
    name: str

    class Config:
        orm_mode = True


class CreateFungus(BaseModel):
    name: str


class FungusORM(ORMBase):
    __tablename__ = "fungus"
    id = Column(Integer, primary_key=True)
    name = Column(String)

以下是这些型号的一个小用法演示:

from typing_extensions import reveal_type

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# ... import CRUDInterface, ORMBase, FungusORM, Fungus, CreateFungus


def main() -> None:
    engine = create_engine("sqlite:///", echo=True)
    ORMBase.metadata.create_all(engine)
    fungus_crud = CRUDInterface(FungusORM, Fungus, CreateFungus)
    with Session(engine) as db:
        mushroom = fungus_crud.create(CreateFungus(name="oyster mushroom"), db)
        reveal_type(mushroom)
        assert mushroom.id == 1

        same_mushroom = fungus_crud.get(1, db)
        reveal_type(same_mushroom)
        assert mushroom == same_mushroom

        all_mushrooms = fungus_crud.list(db)
        reveal_type(all_mushrooms)
        assert all_mushrooms[0] == mushroom

        num_deleted = fungus_crud.delete(1, db)
        assert num_deleted == 1
        all_mushrooms = fungus_crud.list(db)
        assert len(all_mushrooms) == 0


if __name__ == "__main__":
    main()

它的执行没有错误,生成的SQL查询与我们预期的一样.此外,在此代码上运行mypy --strict不会产生任何错误(至少对于SQLAlchemy Mypy plugin),并且也会如预期的那样显示types:

note: Revealed type is "Fungus"
note: Revealed type is "Fungus"
note: Revealed type is "builtins.list[Fungus]"

这清楚地展示了使用泛型相对于简单地使用公共基类或元类进行注释的优势.

如果只是用pydantic.BaseModel注释CRUDInterface.get,类型判断器永远不能告诉您将返回哪个specific模型实例.实际上,这意味着您的IDE不会为您提供有关Fungus模型的specific个方法/属性的任何建议.

显然,在这种情况下,您可以对泛型做更多的工作.您还可以使现有方法更加灵活,等等.但这个例子至少应该能让你开始学习.

Python相关问答推荐

如何推迟对没有公钥的视图/表的反射?

有没有办法清除气流中的僵尸

如何以实现以下所述的预期行为的方式添加两只Pandas pyramme

Python中的Pool.starmap异常处理

如何在Python中按组应用简单的线性回归?

将从Python接收的原始字节图像数据转换为C++ Qt QIcon以显示在QStandardProject中

请从Python访问kivy子部件的功能需要帮助

Python中的函数中是否有充分的理由接受float而不接受int?

Polars:使用列值引用when / then表达中的其他列

如何处理嵌套的SON?

Django管理面板显示字段最大长度而不是字段名称

使用新的类型语法正确注释ParamSecdecorator (3.12)

运行终端命令时出现问题:pip start anonymous"

PMMLPipeline._ fit()需要2到3个位置参数,但给出了4个位置参数

为什么抓取的HTML与浏览器判断的元素不同?

提取相关行的最快方法—pandas

Django RawSQL注释字段

如何在TensorFlow中分类多个类

如何在PySide/Qt QColumbnView中删除列

如何使用OpenGL使球体遵循Python中的八样路径?