138 lines
4.2 KiB
Markdown
138 lines
4.2 KiB
Markdown
# 数据库会话
|
||
|
||
:::tip 说明
|
||
通过 `contextvars` + Starlette 中间件实现请求级数据库会话管理,写操作按业务成功与否自动 commit/rollback。核心代码位于 `src/core/database.py` 和 `src/middleware/db_session.py`。
|
||
:::
|
||
|
||
## 核心架构
|
||
|
||
数据库会话管理位于 `src/core/database.py` 和 `src/middleware/db_session.py`。
|
||
|
||
### contextvars 隔离
|
||
|
||
```python
|
||
from contextvars import ContextVar
|
||
|
||
# 请求级数据库 session 上下文变量
|
||
_db_ctx: ContextVar = ContextVar('_db_ctx')
|
||
```
|
||
|
||
每个请求通过 `contextvars` 持有独立的数据库会话,协程之间互不干扰。
|
||
|
||
### get_db_session()
|
||
|
||
业务代码通过 `get_db_session()` 获取当前请求的数据库会话:
|
||
|
||
```python
|
||
def get_db_session():
|
||
"""获取当前请求的数据库 session(仅限请求上下文内调用)"""
|
||
try:
|
||
return _db_ctx.get()
|
||
except LookupError:
|
||
raise RuntimeError(
|
||
"get_db_session() 只能在请求上下文内调用(缺少 db_session 中间件注入)。"
|
||
"后台线程/启动阶段请使用 SessionLocal() 创建独立会话。"
|
||
) from None
|
||
```
|
||
|
||
:::tip 温馨提示
|
||
`get_db_session()` 只能在请求上下文内调用。后台线程或启动阶段需要使用 `SessionLocal()` 创建独立会话。
|
||
:::
|
||
|
||
## 会话中间件
|
||
|
||
`src/middleware/db_session.py` 中的中间件负责会话的创建、提交和关闭:
|
||
|
||
```python
|
||
async def db_session_middleware(request: Request, call_next):
|
||
"""请求级 DB 会话:写操作按业务成功与否 commit/rollback,finally 关闭"""
|
||
db = SessionLocal()
|
||
_db_ctx.set(db)
|
||
try:
|
||
response = await call_next(request)
|
||
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
|
||
body, consumed = await read_response_body(response)
|
||
if consumed:
|
||
response = rebuild_response(response, body)
|
||
if _is_business_success(body):
|
||
db.commit()
|
||
else:
|
||
db.rollback()
|
||
return response
|
||
except Exception:
|
||
db.rollback()
|
||
raise
|
||
finally:
|
||
db.close()
|
||
```
|
||
|
||
### 业务成功判断
|
||
|
||
中间件通过响应体 JSON 的 `code` 字段判断业务是否成功:
|
||
|
||
```python
|
||
def _is_business_success(body: bytes) -> bool:
|
||
"""仅当响应体为 JSON 且业务码 code == 0 视为成功"""
|
||
if not body:
|
||
return False
|
||
try:
|
||
data = json.loads(body)
|
||
except Exception:
|
||
return False
|
||
if not isinstance(data, dict):
|
||
return False
|
||
return data.get('code') == 0
|
||
```
|
||
|
||
## 独立提交
|
||
|
||
`commit_independently()` 函数用于审计日志等需要无论业务成功与否都持久化的场景:
|
||
|
||
```python
|
||
def commit_independently(obj):
|
||
"""
|
||
使用独立session提交对象,不受请求事务影响
|
||
|
||
日志等审计数据无论业务成功或失败都应持久化,
|
||
因此需要绕过 AOP 事务中间件的 commit/rollback 机制。
|
||
"""
|
||
independent_session = SessionLocal()
|
||
try:
|
||
independent_session.add(obj)
|
||
independent_session.commit()
|
||
except Exception:
|
||
independent_session.rollback()
|
||
raise
|
||
finally:
|
||
independent_session.close()
|
||
```
|
||
|
||
:::warning 使用场景
|
||
`commit_independently()` 创建独立会话,不受请求级事务影响。适用于操作日志、登录日志等审计数据的持久化。普通业务数据应使用 `get_db_session()` 获取请求级会话,由中间件统一管理事务。
|
||
:::
|
||
|
||
## 会话生命周期
|
||
|
||
```
|
||
请求进入
|
||
→ 中间件创建 SessionLocal 实例
|
||
→ 注入 _db_ctx
|
||
→ 业务代码通过 get_db_session() 获取会话
|
||
→ 中间件根据响应码 commit/rollback
|
||
→ finally 关闭会话
|
||
```
|
||
|
||
## 中间件注册顺序
|
||
|
||
在 `src/core/app.py` 中,中间件按以下顺序注册:
|
||
|
||
```
|
||
CORS → redis → rate_limit → db_session → operation_log → logger
|
||
```
|
||
|
||
`db_session` 在 `rate_limit` 之后,确保限流拦截的请求不会创建数据库会话。
|
||
|
||
## 总结
|
||
|
||
数据库会话管理通过 `contextvars` 实现请求级隔离,中间件自动根据业务响应码 commit/rollback,`commit_independently()` 提供独立事务能力用于审计数据持久化。这种设计确保了事务边界清晰,且不会阻塞事件循环。
|