Posted on:
Last modified:
FastAPI 的依赖注入踩了两个坑,加起来大概花了一整天的时间处理。这里记录下,以便后来者能够 快速脱坑。
在 FastAPI 中的官方文档中给了这样一个例子,展示了如何使用依赖注入获取数据库连接:
def get_db():
try:
db = Session()
yield db
finally:
db.close()
@app.put("/users/me")
def update_me(req: UserIn, db=Depends(get_db)):
db.update(req)
db.commit() # <-- 感觉每个函数都写有点累赘
...
我一看,这个很好啊,但是每个函数里都要 commit 太麻烦了,不如一起放到 get_db 这个依赖里吧, 结果就被坑了。
# 错误的代码!!!
async def get_db():
try:
db = Session()
yield db
db.commit() # 在这里 commit 并不合适!
finally:
db.close()
让我们来分析一下错在了哪里:
handler 函数(@app.get 装饰的函数)和依赖函数的执行顺序是:
app
-> handler 参数中有依赖
-> depends 开始执行
-> yield 返回 db 链接
-> handler 继续执行
-> handler return
-> depends 执行 yield 下一句
如果把 commit 放在依赖中,那么 commit 的执行在 handler return 后面,这就会导致两个严重隐患:
虽然在最后添加了 session.commit(),但是这个操作是在发送完 http 响应之后才操作的,也就是说 这时候还没有写入到数据库中。如果马上请求对应数据,可能得到的不是最新的结果。
在前端使用 useSWR 的 mutate 更新前端数据的时候就遇到了这个问题。
更可怕的是,如果 commit 失败,但是用户从 http 响应中已经得到了成功的消息,会直接导致数据 不一致。比如说用户以为下单成功了,结果却没有成功。
把 commit 放在业务代码中,这样才能确保每一个请求之间以及数据库和前端的状态都是一致的。
这里得到的教训是:不要在依赖中 commit
当我们在 handler 函数中遇到异常的时候,一般需要回滚数据库。我又转念一想,这个简单啊!直接 在 get_db 里捕获异常然后 rollback 不就得了,结果又被坑了。在依赖中 catch all 会造成异常 被吞掉。
# 错误的代码!!!
async def get_db():
try:
db = Session()
yield db
except Exception: # <-- 在这里捕获所有异常并不合适!
db.rollback()
finally:
db.close()
正确的代码:
except Exception:
db.rollback()
# 如果捕获了,一定要 raise 出来
+ raise
finally:
道理和前面一样,except 语句在 handler 函数之后运行,如果抛出了异常,那么就会在依赖函数中 被捕获到,如果这里还没有 raise 出来,那么这个异常就根本不会在其他地方(如日志中)处理了。
这里得到的教训是:不要在依赖中捕获所有异常
总而言之,使用 yield 的依赖,类似于在 yield 语句位置调用了 handler 函数。理解了这一点, 就不会犯上述两个错误。
# 一个正确的依赖方法
async def get_db():
try:
db = Session()
# 相当于 handler(db), yield db 传递了控制,也就相当于在这里调用了 handler
yield db # <-- handler(db)
# <-- 不要在这里添加 commit
except Exception:
db.rollback()
raise # <-- 一定要 raise 出来
finally:
db.close()
© 2016-2022 Yifei Kong. Powered by ynotes
All contents are under the CC-BY-NC-SA license, if not otherwise specified.
Opinions expressed here are solely my own and do not express the views or opinions of my employer.
友情链接: MySQL 教程站