我真的对我所看到的行为感到困惑,所以我希望有人能够阐明这里发生的事情。基本上,我有一个在后端使用 FastAPI 和 SQLAlchemy 的 Web 应用程序,其中涉及用户发布对调查的回复。在接受响应的 post 请求的端点中,会在数据库中创建一个
Response
对象,然后在数据库中创建许多 Match
对象,并使用所创建的 Response
对象的外键。在从有效负载解析和创建 Match
对象的过程中,我聚合了 number_correct_matches
变量(这是一个正整数),然后更新 Response
对象以通过 CRUD 中的 SQLAlchemy 会话包含 number_correct_matches
具有 update
函数的类。
令我困惑的行为是,当我执行执行此更新操作的代码时,无论是在我正在开发的树莓派 4 上本地运行完整项目(使用本地 postgresql 数据库)还是通过单元测试(使用内存中的 sqlite 数据库),数据库中的更新不会发生在
Response
对象上。但是,更新在两种情况下有效:
如果我在 vscode 中以调试模式运行单元测试,并一次一行地执行更新函数中的 for 循环,则
Response
对象会在数据库中正确更新。
如果我在更新函数中放入记录器语句,并且如果日志记录语句打印正在更新的
db_object
,则Response
对象已正确更新。如果我放置不包含 db_object
的记录器语句,记录器语句将输出到日志,但对象不会更新。
预期的行为仅发生在上述两种情况中的任何一个,否则代码实际上会被跳过(请参阅下面的内容)。此外,我在应用程序中还有其他模型,可以成功地对不同的属性使用相同的更新函数(例如,使用相同函数的用户对象有一个更新端点,我可以通过它成功更新用户的密码)。
我不明白代码似乎只有在调试模式下直接单步执行或打印 db_object 参数时才会执行。我知道我可以在数据库中创建它们之前循环遍历有效负载中的
Match
对象,只是为了获取 number_correct_matches
值并首先使用该值创建 Response
对象,但我真的很想理解仅当我一次单步执行一行或当我有特定的记录器语句时它如何工作,而不是在没有多余日志的情况下正常运行代码时它如何工作。
api/response.py
response = crud.response.update(
session=session,
db_object=response, # <- this is the already created Response object in the database
object_in=schemas.ResponseUpdate(
number_correct_matches=number_correct_matches
),
)
模式/response.py
# properties to receive via API update
class ResponseUpdate(ResponseBase):
number_correct_matches: int = 0
crud_base.py(由crud_response扩展)
def update(
self,
session: Session,
*,
db_object: ModelType,
object_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> ModelType:
"""
Updates an object in the database
Args:
session (Session): a SQLAlchemy Session object that is connected to the database.
db_object (ModelType): An object from the database.
object_in (Union[UpdateSchemaType, Dict[str, Any]]): A pydantic model object used to update the db model object.
Returns:
ModelType: The updated db model object.
"""
# logger.info(f"updating {db_object}") <- with just this logging statement, it works correctly
logger.info("updating 1")
# update the db object
update_data = ( # <- If breakpoint is set here and then I jump to below the for loop without stepping through, it does not work
object_in
if isinstance(object_in, dict)
else object_in.model_dump(exclude_unset=True)
)
for field in jsonable_encoder(db_object): # <- It works if breakpoint is set here and then I walk through one line at a time to the end of the for loop
logger.info("updating 2")
if field in update_data.keys():
logger.info("updating 3")
setattr(db_object, field, update_data[field])
logger.info("updating 4")
# update the db object through the session
session.add(db_object) # <- If breakpoint is set here, it does not work
session.commit()
session.refresh(db_object)
return db_object
上面的代码按原样运行(第一个记录器语句被注释掉)会产生以下日志:
2024-07-12 20:53:52.228 | INFO | app.crud.crud_base:update:82 - updating 1
2024-07-12 20:53:52.229 | INFO | app.crud.crud_base:update:94 - updating 4
我还要补充一点,即使我在响应端点中发布了代码,我也在
conftest.py中调用
crud.response.update
并且我得到了完全相同的行为。
不是完整的答案,只是进一步调试的线索(不适合评论)
我相信正在发生的是这里发生了一些奇怪的缓存边缘情况:
__repr__
,它会产生副作用(某些值会缓存在 db_object
中),这会更改其用于调试的两条引线:
如果在
jsonable_encoder
内添加断点会发生什么。有效吗?如果没有,您可以调查为什么它返回一个空序列
如果更换会发生什么
for field in jsonable_encoder(db_object): # <- It works if breakpoint is set here and then I walk through one line at a time to the end of the for loop
logger.info("updating 2")
if field in update_data.keys():
logger.info("updating 3")
setattr(db_object, field, update_data[field])
与
fields = jsonable_encoder(db_object)
for field in fields: # <- It works if breakpoint is set here and then I walk through one line at a time to the end of the for loop
logger.info("updating 2")
if field in update_data.keys():
logger.info("updating 3")
setattr(db_object, field, update_data[field])
else:
logger.error(f"no fields found: {fields=}, {db_object=}")