OpenHands 源码解析系列
第 13 讲:数据库设计与 ORM 层
基于 OpenHands 源码 · 2026-08-09
一、数据库架构概览
OpenHands 的数据库层是 V1 App Server 的核心基础设施。它同时支持PostgreSQL(生产环境)和 SQLite(本地开发),通过 SQLAlchemy 2.0 的 DeclarativeBase 构建了完整的 ORM 抽象。数据库层的设计围绕三个核心目标:
🔹 双引擎支持:同步 pg8000 + 异步 asyncpg,同一套模型两种驱动
🔹 请求级会话:每个 HTTP 请求一个 AsyncSession,自动 commit/rollback
🔹 渐进式迁移:Alembic 14 次迁移,从 V0 到 V1 平滑演进
📦 核心文件
app_server/services/db_session.py — DbSessionInjector 配置与引擎管理
app_server/utils/sql_utils.py — Base 类与 TypeDecorator 工具
app_server/services/db_session_injector.py — FastAPI DI 桥接
db/ssl.py — SSL 模式标准化
app_lifespan/alembic/ — 14 次迁移脚本
二、DbSessionInjector:数据库配置的集中化管理
OpenHands 没有使用传统的 SQLAlchemy.create_engine() 裸调用,而是将所有数据库配置封装在 DbSessionInjector 中。这个类继承自 Pydantic BaseModel 和自定义的 Injector[AsyncSession] 接口,实现了配置校验、引擎创建、会话管理的三位一体。
2.1 配置模型与 Pydantic 校验
📄 app_server/services/db_session.py (第 29-52 行)
class DbSessionInjector(BaseModel, Injector[AsyncSession]):
persistence_dir: Path
host: str | None = None
port: int | None = None
name: str | None = None
user: str | None = None
password: SecretStr | None = None
echo: bool = False
pool_size: int = 25
max_overflow: int = 10
pool_recycle: int = 1800
pool_use_lifo: bool = True
gcp_db_instance: str | None = None
gcp_project: str | None = None
gcp_region: str | None = None
ssl_mode: str | None = None
# Private attrs
_engine: Engine | None = PrivateAttr(default=None)
_async_engine: AsyncEngine | None = PrivateAttr(default=None)
_session_maker: sessionmaker | None = PrivateAttr(default=None)
_async_session_maker: async_sessionmaker | None = PrivateAttr(default=None)
_gcp_connector: Any = PrivateAttr(default=None)设计要点:
🔹 SecretStr 保护密码字段,序列化时自动脱敏
🔹 PrivateAttr 存放运行时引擎实例,不参与 Pydantic 序列化
🔹 连接池默认 25 + 10 overflow,1800 秒回收,LIFO 策略(减少冷启动延迟)
🔹 GCP Cloud SQL 专用字段支持 Google Cloud 托管数据库
2.2 环境变量兼容层
fill_empty_fields 模型校验器实现了向后兼容——旧版部署只设环境变量,新版部署可以直接传配置对象:
📄 app_server/services/db_session.py (第 54-81 行)
@model_validator(mode='after')
def fill_empty_fields(self):
"""Override any defaults with values from legacy environment variables"""
if self.host is None:
self.host = os.getenv('DB_HOST')
if self.port is None:
self.port = int(os.getenv('DB_PORT', '5432'))
if self.name is None:
self.name = os.getenv('DB_NAME', 'openhands')
if self.user is None:
self.user = os.getenv('DB_USER', 'postgres')
if self.password is None:
self.password = SecretStr(os.getenv('DB_PASS', 'postgres').strip())
# ... GCP 和 SSL 配置同样从环境变量读取
return self2.3 三套引擎:普通 / GCP / SQLite
get_async_db_engine() 方法根据配置选择三种引擎路径:
📄 app_server/services/db_session.py (第 177-222 行)
async def get_async_db_engine(self) -> AsyncEngine:
async_engine = self._async_engine
if async_engine:
return async_engine
if self.gcp_db_instance: # GCP environments
async_engine = await self._create_async_gcp_engine()
else:
url: str | URL
if self.host:
# PostgreSQL with asyncpg driver
url = URL.create(
'postgresql+asyncpg',
username=self.user or '',
password=password,
host=self.host,
port=self.port,
database=self.name,
)
else:
# SQLite fallback for local development
url = f'sqlite+aiosqlite:///{str(self.persistence_dir)}/openhands.db'
if self.host:
async_engine = create_async_engine(
url,
connect_args=build_asyncpg_connect_args(self.ssl_mode),
pool_size=self.pool_size,
max_overflow=self.max_overflow,
pool_recycle=self.pool_recycle,
pool_pre_ping=True,
pool_use_lifo=self.pool_use_lifo,
)
else:
async_engine = create_async_engine(
url,
poolclass=NullPool, # SQLite 不需要连接池
pool_pre_ping=True,
)关键设计决策:
🔹 SQLite 使用 NullPool:SQLite 不支持并发连接,用 NullPool 避免连接池竞争
🔹 PostgreSQL 使用 asyncpg:原生异步驱动,性能优于 aiopg
🔹 GCP Cloud SQL 专用通道:通过 google.cloud.sql.connector 建立 IAM 认证连接,无需明文密码
🔹 pool_pre_ping=True:每次借用连接前检测是否存活,防止 stale connection
三、请求级会话管理
OpenHands 的会话管理采用了两种模式:Injector 注入模式(推荐)和 FastAPI Depends 模式(兼容旧代码)。
3.1 Injector 注入模式 — 自动事务管理
📄 app_server/services/db_session.py (第 298-341 行)
async def inject(
self, state: InjectorState, request: Request | None = None
) -> AsyncGenerator[AsyncSession, None]:
"""Dependency function that manages database sessions through request state."""
db_session = getattr(state, DB_SESSION_ATTR, None)
if db_session:
yield db_session
else:
# Create a new session and store it in request state
session_maker = await self.get_async_session_maker()
db_session = session_maker()
try:
setattr(state, DB_SESSION_ATTR, db_session)
yield db_session
if not getattr(state, DB_SESSION_KEEP_OPEN_ATTR, False):
await db_session.commit()
except Exception:
_logger.exception('Rolling back SQL due to error', stack_info=True)
await db_session.rollback()
raise
finally:
if not getattr(state, DB_SESSION_KEEP_OPEN_ATTR, False):
if hasattr(state, DB_SESSION_ATTR):
delattr(state, DB_SESSION_ATTR)
with contextlib.suppress(Exception):
await db_session.close()事务生命周期:
🔹 首次注入创建:同一请求多次注入返回同一个 session(通过 state 属性缓存)
🔹 成功自动 commit:yield 后无异常自动提交
🔹 异常自动 rollback:任何异常触发回滚并记录日志
🔹 finally 清理:session 关闭 + state 属性清除,防止泄漏
🔹 DB_SESSION_KEEP_OPEN_ATTR:特殊标志位允许跨请求保持 session(用于长连接场景)
3.2 FastAPI Depends 桥接层
为避免循环导入,depends_db_session() 被放在独立的 db_session_injector.py 中:
📄 app_server/services/db_session_injector.py (第 40-74 行)
def get_db_session(
state: InjectorState, request: Request | None = None
) -> AsyncContextManager[AsyncSession]:
"""Return an async context manager yielding the request-scoped AsyncSession."""
from openhands.app_server.config import get_global_config
return get_global_config().db_session.context(state, request)
async def _yield_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
"""Per-request FastAPI dependency yielding the request-scoped AsyncSession.
get_global_config is looked up at request time -- not at module load --
so that this module is safe to evaluate from inside config_from_env.
"""
from openhands.app_server.config import get_global_config
async for db_session in get_global_config().db_session.depends(request):
yield db_session
def depends_db_session():
"""FastAPI Depends(...) factory for the request-scoped AsyncSession."""
return Depends(_yield_db_session)为什么需要这个桥接层?源码注释解释得很清楚:
如果功能模块直接导入 app_server.config,而 config 又通过 env_parser 动态加载功能模块,就会形成循环导入。将 DI helpers 放在独立的、无依赖的模块中,功能代码可以获取 AsyncSession 而不引入 app_server.config。
四、ORM 模型设计
4.1 Base 类与 SQLAlchemy 2.0 风格
📄 app_server/utils/sql_utils.py (第 1-19 行)
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
"""
Base class for all SQLAlchemy models.
Uses SQLAlchemy 2.0 DeclarativeBase for proper type inference with Mapped types.
This is backward compatible with existing Column() definitions while enabling
gradual migration to mapped_column() with Mapped[T] type annotations.
"""
pass所有 ORM 模型继承自 Base,使用 SQLAlchemy 2.0 的 Mapped[T] + mapped_column() 新语法,同时兼容旧版 Column() 定义。这种混合策略允许渐进式迁移。
4.2 自定义 TypeDecorator 体系
OpenHands 定义了 4 种自定义类型装饰器,覆盖常见的数据持久化场景:
| TypeDecorator | 底层类型 | 用途 |
|---|---|---|
create_json_type_decorator(T) | JSON | Pydantic 模型 ↔ JSON 列序列化 |
StoredSecretStr | String | JWE 加密存储敏感字符串 |
UtcDateTime | DateTime(timezone=True) | 强制 UTC 时区转换 |
create_enum_type_decorator(E) | String | 枚举值 ↔ 字符串互转 |
StoredSecretStr 的加密机制:
📄 app_server/utils/sql_utils.py (第 44-70 行)
class StoredSecretStr(TypeDecorator):
"""TypeDecorator for secret strings. Encrypts before storing."""
impl = String
cache_ok = True
def process_bind_param(self, value, dialect):
if value is not None:
jwt_service = get_global_config().jwt.get_jwt_service()
token = jwt_service.create_jwe_token({'v': value.get_secret_value()})
return token
return None
def process_result_param(self, value, dialect):
if value is not None:
jwt_service = get_global_config().jwt.get_jwt_service()
token = jwt_service.decrypt_jwe_token(value)
return SecretStr(token['v'])
return None写入时通过 JWT service 创建 JWE 加密令牌,读取时解密还原。这意味着即使数据库被直接查询,敏感字段也不会暴露。
4.3 核心数据模型
OpenHands 当前有 7 张核心表,分布在不同的业务模块中:
| 表名 | 模型类 | 主键 | 用途 |
|---|---|---|---|
conversation_metadata | StoredConversationMetadata | conversation_id | 会话元数据(核心表) |
conversation_cost_events | StoredConversationCostEvent | id (Identity) | 费用事件追踪 |
app_conversation_start_task | StoredAppConversationStartTask | id (UUID) | 会话启动任务队列 |
event_callback | StoredEventCallback | id (UUID) | Webhook 回调注册 |
event_callback_result | StoredEventCallbackResult | id (UUID) | 回调执行结果 |
v1_remote_sandbox | StoredRemoteSandbox | id (String) | 远程沙箱本地缓存 |
recaptcha_logs | (纯迁移定义) | id (String) | reCAPTCHA 验证日志 |
4.4 conversation_metadata — 核心表的完整结构
📄 sql_app_conversation_info_service.py (第 132-190 行)
class StoredConversationMetadata(Base):
__tablename__ = 'conversation_metadata'
conversation_id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: str(uuid.uuid4())
)
selected_repository: Mapped[str | None] = mapped_column(String, nullable=True)
selected_branch: Mapped[str | None] = mapped_column(String, nullable=True)
git_provider: Mapped[str | None] = mapped_column(String, nullable=True)
title: Mapped[str | None] = mapped_column(String, nullable=True)
last_updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), default=utc_now
)
created_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), default=utc_now
)
trigger: Mapped[str | None] = mapped_column(String, nullable=True)
pr_number: Mapped[list[int] | None] = mapped_column(
create_json_type_decorator(list[int])
)
# Cost and token metrics
accumulated_cost: Mapped[float | None] = mapped_column(default=0.0)
prompt_tokens: Mapped[int | None] = mapped_column(default=0)
completion_tokens: Mapped[int | None] = mapped_column(default=0)
total_tokens: Mapped[int | None] = mapped_column(default=0)
max_budget_per_task: Mapped[float | None] = mapped_column(nullable=True)
cache_read_tokens: Mapped[int | None] = mapped_column(default=0)
cache_write_tokens: Mapped[int | None] = mapped_column(default=0)
reasoning_tokens: Mapped[int | None] = mapped_column(default=0)
context_window: Mapped[int | None] = mapped_column(default=0)
per_turn_token: Mapped[int | None] = mapped_column(default=0)
llm_model: Mapped[str | None] = mapped_column(String, nullable=True)
agent_kind: Mapped[str | None] = mapped_column(String, nullable=True)
conversation_version: Mapped[str] = mapped_column(
String, nullable=False, default='V0', index=True
)
sandbox_id: Mapped[str | None] = mapped_column(String, nullable=True, index=True)
parent_conversation_id: Mapped[str | None] = mapped_column(
String, nullable=True, index=True
)
public: Mapped[bool | None] = mapped_column(nullable=True, index=True)
execution_status: Mapped[str | None] = mapped_column(
String, nullable=True, index=True
)
tags: Mapped[dict[str, str] | None] = mapped_column(
create_json_type_decorator(dict[str, str]), nullable=True
)字段分组解读:
🔹 标识字段:conversation_id (PK)、sandbox_id (indexed)、parent_conversation_id (indexed)
🔹 Git 集成:selected_repository、selected_branch、git_provider、pr_number (JSON)
🔹 费用统计:accumulated_cost、prompt/completion/cache_read/cache_write/reasoning tokens
🔹 运行时状态:execution_status (idle/running/paused/finished/error/stuck/deleting)
🔹 扩展标签:tags (JSON dict) 支持自动化上下文、技能使用记录
五、Alembic 迁移历史
OpenHands 使用 Alembic 管理数据库迁移,alembic/env.py 从全局配置获取数据库 URL,支持在线和离线两种模式。14 次迁移记录了从 2025 年 10 月到 2026 年 6 月的 schema 演进:
| 迁移 | 日期 | 变更内容 |
|---|---|---|
| 001 | 2025-10 | 初始建表:5 张核心表 |
| 002 | 2025-10 | 修正表结构对齐模型定义 |
| 003 | 2025-11 | 添加 parent_conversation_id 支持子会话 |
| 004 | 2026-01 | 添加 public 列支持公开会话 |
| 005 | 2025-11 | 对齐 StoredConversationMetadata 数据类 |
| 006 | 2026-02 | 添加 session_api_key_hash 到远程沙箱 |
| 007 | 2026-03 | 新增 pending_messages 服务器消息队列 |
| 008 | 2026-03 | 添加 tags 列 (JSON) 支持会话标签 |
| 009 | 2026-04 | 添加 agent_kind 列 |
| 010 | 2026-05 | 添加 conversation_cost_events 费用表 |
| 011 | 2026-06 | 添加 is_paused 到远程沙箱 |
| 012 | 2026-06 | 回退:移除 is_paused 列 |
| 013 | 2026-06 | 添加 execution_status 到会话元数据 |
014 | 2026-06 | 新增 recaptcha_logs 反机器人日志表 |
迁移模式观察:
🔹 011→012 回退模式:is_paused 列在 011 添加后 3 天就被 012 移除,说明团队在快速迭代中采用了"先加后删"的策略
🔹 autogenerate 支持:env.py 显式导入所有模型类,使 Alembic 能自动检测 schema 差异
🔹 双模式运行:在线模式用真实引擎连接,离线模式生成纯 SQL 脚本
六、加密与安全机制
6.1 密钥管理
📄 app_server/utils/encryption_key.py (第 13-86 行)
class EncryptionKey(BaseModel):
id: str = Field(default_factory=lambda: base62.encodebytes(os.urandom(32)))
key: SecretStr
active: bool = True
notes: str | None = None
created_at: datetime.datetime = Field(default_factory=utc_now)
def get_default_encryption_keys(workspace_dir: Path) -> list[EncryptionKey]:
# Priority 1: JWT_SECRET env var (deterministic key ID for multi-pod)
jwt_secret = os.getenv('JWT_SECRET')
if jwt_secret:
key_id = base62.encodebytes(hashlib.sha256(jwt_secret.encode()).digest())
return [EncryptionKey(id=key_id, key=SecretStr(jwt_secret), ...)]
# Priority 2: .keys file (persisted key rotation)
# Priority 3: .jwt_secret file (legacy fallback)
# Priority 4: Generate new random key密钥优先级链:
🔹 环境变量 JWT_SECRET:多 Pod 部署时通过 SHA-256 派生确定性 key_id,确保不同 Pod 用相同 ID 加密/解密
🔹 .keys 文件:持久化的密钥轮换记录,支持多个密钥同时有效
🔹 .jwt_secret 文件:遗留系统兼容
🔹 随机生成:最后手段,生成 32 字节随机密钥
6.2 SSL/TLS 配置
📄 db/ssl.py (第 1-41 行)
SUPPORTED_DB_SSL_MODES: frozenset[str] = frozenset({'prefer', 'require', 'disable'})
def build_pg8000_connect_args(db_ssl_mode: str | None) -> dict[str, bool]:
# sync driver: ssl_context = True/False
mode = normalize_db_ssl_mode(db_ssl_mode)
if mode == 'require':
return {'ssl_context': True}
if mode == 'disable':
return {'ssl_context': False}
return {}
def build_asyncpg_connect_args(db_ssl_mode: str | None) -> dict[str, str]:
# async driver: ssl = 'require'/'disable'
mode = normalize_db_ssl_mode(db_ssl_mode)
if mode:
return {'ssl': mode}
return {}SSL 配置同时适配 pg8000(同步)和 asyncpg(异步)两种驱动的 API 差异,确保同一配置在两种模式下行为一致。
七、架构总结
数据库层架构总览
FastAPI Request
│
▼
┌─────────────────────────────────────────┐
│ depends_db_session() / inject() │ ◄── DI 层
│ ┌───────────────────────────────────┐ │
│ │ DbSessionInjector │ │ ◄── 配置 + 引擎管理
│ │ ├── Pydantic config model │ │
│ │ ├── fill_empty_fields(env) │ │
│ │ ├── get_async_db_engine() │ │
│ │ │ ├── PostgreSQL (asyncpg) │ │
│ │ │ ├── GCP Cloud SQL │ │
│ │ │ └── SQLite (aiosqlite) │ │
│ │ └── inject() → AsyncSession │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Base (DeclarativeBase) │ │ ◄── ORM 层
│ │ ├── StoredConversationMetadata │ │
│ │ ├── StoredConversationCostEvent │ │
│ │ ├── StoredAppConversationStartTask│ │
│ │ ├── StoredEventCallback │ │
│ │ ├── StoredEventCallbackResult │ │
│ │ └── StoredRemoteSandbox │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ TypeDecorators │ │ ◄── 类型转换层
│ │ ├── StoredSecretStr (JWE) │ │
│ │ ├── UtcDateTime │ │
│ │ ├── JsonTypeDecorator │ │
│ │ └── EnumTypeDecorator │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
PostgreSQL / SQLite设计亮点:
🔹 配置即模型:DbSessionInjector 用 Pydantic 做配置校验,环境变量与显式配置双通道
🔹 请求级事务:自动 commit/rollback,state 属性缓存避免重复创建
🔹 引擎三重路径:PostgreSQL / GCP Cloud SQL / SQLite 按需切换
🔹 安全内建:SecretStr + JWE 加密 + SSL 标准化
🔹 迁移可追溯:14 次 Alembic 迁移完整记录 schema 演进
📚 系列导航
← 第 12 讲:配置系统与多环境支持
→ 第 14 讲:API 路由设计与中间件
关注公众号「AI技术推荐官」获取更多源码解析内容
夜雨聆风