MongoDB 的 FastAPI 问题 - TypeError: 'ObjectId' 对象不可迭代

Sof*_*ngs 6 python mongodb pymongo fastapi

我在通过 FastAPI 插入 MongoDB 时遇到一些问题。

下面的代码按预期工作。请注意该response变量尚未在 中使用response_to_mongo()

model是一个 sklearn ElasticNet 模型。

app = FastAPI()


def response_to_mongo(r: dict):
    client = pymongo.MongoClient("mongodb://mongo:27017")
    db = client["models"]
    model_collection = db["example-model"]
    model_collection.insert_one(r)


@app.post("/predict")
async def predict_model(features: List[float]):

    prediction = model.predict(
        pd.DataFrame(
            [features],
            columns=model.feature_names_in_,
        )
    )

    response = {"predictions": prediction.tolist()}
    response_to_mongo(
        {"predictions": prediction.tolist()},
    )
    return response
Run Code Online (Sandbox Code Playgroud)

但是,当我predict_model()这样写并将response变量传递给response_to_mongo()

@app.post("/predict")
async def predict_model(features: List[float]):

    prediction = model.predict(
        pd.DataFrame(
            [features],
            columns=model.feature_names_in_,
        )
    )

    response = {"predictions": prediction.tolist()}
    response_to_mongo(
        response,
    )
    return response
Run Code Online (Sandbox Code Playgroud)

我收到一条错误消息:

TypeError: 'ObjectId' object is not iterable
Run Code Online (Sandbox Code Playgroud)

从我的阅读来看,这似乎是由于 FastAPI 和 Mongo 之间的 BSON/JSON 问题造成的。但是,为什么当我不使用变量时它在第一种情况下起作用?这是由于 FastAPI 的异步特性吗?

Chr*_*ris 13

根据文档

\n
\n

插入文档时,"_id"如果文档\xe2\x80\x99t 尚未包含密钥,则会自动添加特殊密钥"_id",。该值"_id"在整个集合中必须是唯一的。返回InsertOneResultinsert_one()的\实例。有关“_id”的详细信息,请参阅有关 _id 的\n 文档

\n
\n

因此,在您提供的示例的第二种情况下,当您将字典传递给函数时,Pymongo 将向您的字典添加从数据库检索数据所需的insert_one()唯一标识符(即);ObjectId因此,当从端点返回响应时,ObjectId序列化失败\xe2\x80\x94,因为,如本答案中详细描述的,FastAPI默认情况下会使用jsonable_encoder(到确保不可序列化的对象转换为 a str),然后返回 a JSONResponse,它使用标准json库来序列化数据。

\n

解决方案1

\n

使用此处演示的方法,通过默认ObjectId转换为str,因此,您可以response在端点内照常返回。

\n
# place these at the top of your .py file\nimport pydantic\nfrom bson import ObjectId\npydantic.json.ENCODERS_BY_TYPE[ObjectId]=str\n\nreturn response # as usual\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案2

\n

将加载的内容转储BSON到有效JSON字符串,然后将其重新加载为dict,如此处和此处所述

\n
from bson import json_util\nimport json\n\nresponse = json.loads(json_util.dumps(response))\nreturn response\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案3

\n

定义一个自定义JSONEncoder,如此处所述将其转换ObjectIdstr

\n
import json\nfrom bson import ObjectId\n\nclass JSONEncoder(json.JSONEncoder):\n    def default(self, o):\n        if isinstance(o, ObjectId):\n            return str(o)\n        return json.JSONEncoder.default(self, o)\n\n\nresponse = JSONEncoder().encode(response)\nreturn response\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案4

\n

您可以拥有一个不带“ObjectId”( _id) 字段的单独输出模型,如文档中所述。response_model您可以使用端点装饰器中的参数来声明用于响应的模型。例子:

\n
from pydantic import BaseModel\n\nclass ResponseBody(BaseModel):\n    name: str\n    age: int\n\n\n@app.get(\'/\', response_model=ResponseBody)\ndef main():\n    # response sample\n    response = {\'_id\': ObjectId(\'53ad61aa06998f07cee687c3\'), \'name\': \'John\', \'age\': \'25\'}\n    return response\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案5

\n

在返回之前"_id"从字典中删除条目(请参阅此处了解如何从 a 中删除键):responsedict

\n
response.pop(\'_id\', None)\nreturn response\n
Run Code Online (Sandbox Code Playgroud)\n