$ ls ~yifei/notes/

FastAPI 依赖注入的两个坑

Posted on:

Last modified:

FastAPI 的依赖注入踩了两个坑,加起来大概花了一整天的时间处理。这里记录下,以便后来者能够 快速脱坑。

  1. 在依赖中同时使用 yield db 和 commit ❌
  2. 在依赖中使用 except Exception 捕获所有异常 ❌

在依赖中 commit

在 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 后面,这就会导致两个严重隐患:

  1. 已经 return 给前端 200 OK 了,结果 commit 失败;
  2. 数据还没有 commit 到数据库,另一个请求已经来读数据了,没有读取到新数据。

虽然在最后添加了 session.commit(),但是这个操作是在发送完 http 响应之后才操作的,也就是说 这时候还没有写入到数据库中。如果马上请求对应数据,可能得到的不是最新的结果。

在前端使用 useSWR 的 mutate 更新前端数据的时候就遇到了这个问题。

更可怕的是,如果 commit 失败,但是用户从 http 响应中已经得到了成功的消息,会直接导致数据 不一致。比如说用户以为下单成功了,结果却没有成功。

把 commit 放在业务代码中,这样才能确保每一个请求之间以及数据库和前端的状态都是一致的。

这里得到的教训是:不要在依赖中 commit

在依赖中 catch all

当我们在 handler 函数中遇到异常的时候,一般需要回滚数据库。我又转念一想,这个简单啊!直接 在 get_db 里捕获异常然后 rollback 不就得了,结果又被坑了。在依赖中 catch all 会造成异常 被吞掉。

# 错误的代码!!!
async def get_db():
    try:
        db = Session()
        yield db
    except Exception:  # <-- 在这里捕获所有异常并不合适!
        db.rollback()
        # 如果捕获了,一定要 raise 出来
        # raise
    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.