本页只讲架构关系:哪个进程持有哪个库、库内表怎么关联、跨库引用靠什么、物理隔离边界在哪、关系库又怎么和对象存储配合。具体字段、类型、默认值、约束、索引在数据库表字段参考;可执行 DDL 以各库 schema SQL 为准(data_serverapp/db/schema.sqlapp/db/collection_schema.sqluser_managerapp/db/schema.sql)。 系统是混合持久化data_server 只用 PostgreSQL(无 fallback、无后端开关),user_manager 仍完全用 SQLite。

一图看清:谁持有哪个库

三个物理库分布在两个进程里:local_auth.dbuser_manager,两个 PostgreSQL 库都归 data_servercollection_server 自身不持有任何数据库——它是客户端直连的无状态薄网关,靠用户 Bearer + 服务断言(X-Rakull-Service-Assertion)调 data_server 内部接口,由 data_server 进程唯一读写 rakull_collection
服务(进程)引擎 / 连接表数量
user_managerSQLite · LOCAL_AUTH_DATABASE_PATHlocal_auth.db5
data_serverPostgreSQL · DATA_DATABASE_URLrakull_data58
data_serverPostgreSQL · COLLECTION_DATABASE_URL(独立连接工厂)rakull_collection2

三条必须记住的架构关系

  1. 实线是调用/持有,虚线全是跨库逻辑引用。 虚线不带任何数据库级约束,目标行是否存在数据库不保证,只能在应用层保证。
  2. user_id 是贯穿三库的身份与归属键,但不存在物理外键。 它由 SQLite 侧 users.id 发出,跨引擎被两个 PostgreSQL 库引用(文章、收藏、反馈等都按它归属用户)。多用户隔离完全靠应用代码在查询里带 user_id,不是靠数据库。
  3. 两个 PostgreSQL 库物理隔离,禁止跨库 JOIN。 rakull_collection 只通过 article_id + sentence_index 逻辑引用 rakull_data 里的句子;收藏卡把句子正文快照成 front/back,正是因为不能跨库取原文。

分库实体关系

rakull_data(PostgreSQL,内容库)

58 张表覆盖文章阅读、情景训练、旅程、词汇与自适应学习。下图是日常核心阅读链路的 6 张表:
  • 实线是 PostgreSQL 强制的物理外键(全库共 41 个,此处只画核心链路)。删文章会级联删句子、weekly 记录与收藏。
  • 唯一的虚线是 feedback:它和其它表同库,却故意不声明外键,只在应用层用 article_id + sentence_index 当上下文 ID,因此被反馈的文章/句子可独立删除而不拖累反馈。
  • 文章归属 articles.user_idis_public / guest_visible 决定可见性。可见性是读权限概念,与所有权(能否写)是两回事。
字段与索引见数据库表字段参考rakull_data 一节。

local_auth.db(SQLite,账号与赞助库)

五条线全是同库物理外键(连接统一开 PRAGMA foreign_keys=ON 与 WAL)。注意 donations 挂了两条:user_id 随用户 CASCADE,审核管理员 reviewed_bySET NULL,基数因此不同。users 上后 7 个赞助字段是 donations 的汇总缓存(每次重算而非累加)。这个库对 PostgreSQL 侧的唯一引用就是跨引擎 user_id(已在顶部拓扑图画虚线)。 字段与索引见数据库表字段参考local_auth.db 一节。

rakull_collection(PostgreSQL,收藏域)

库内只有这一条物理外键。collection_items 还引用三类外部 ID,全是跨库逻辑引用(顶部拓扑图虚线):collections.user_id 指向 SQLite 的 usersarticle_id + sentence_index 指向另一个 PG 库 rakull_data。复习调度的 SM-2 状态(due / interval / ease / reps / card_version 乐观锁)都内联在 item 行上,不依赖内容库。 字段与索引见数据库表字段参考rakull_collection 一节。

关系库与对象存储如何配合

结构化行在关系库里,但音频、视频、图片、上传文件这些字节块不在业务表中。关系库与对象存储通过 rakull_data 里的 media_objects 表对接——它是两条存储轴的交汇点:既是被外键引用的关系行,又是字节的账本;而字节真正落在哪由 OBJECT_STORE 决定,业务表始终只持有“指针”。 组合规则:
  • 一张元数据表,两种字节落点。 media_objects 行永远在 PostgreSQL;local 模式字节内联在 data BYTEA(删行即删字节),cos 模式 dataNULL、只在 storage_key 存对象 key,读取时换成 1 小时有效的预签名 URL,删除按 key 引用计数(同一句子、同一声音的 TTS 是内容寻址的共享对象,用 cache_hash 去重,不能在仍被引用时删桶内对象)。
  • 关系库到字节是“弱关联”,不是级联。 sentences.audio_object_id → media_objects.id ON DELETE SET NULL:媒体对象删除后句子仍在,只是丢音频。字节因此可有独立于句子的生命周期与共享复用。
  • 通用文件上传走另一条 files 通道,不登记 media_objects /v1/data/files 通用上传、视频转写源等落到 UPLOAD_DIR 或 COS,对外只给不透明 handle(拒绝 ..、绝对路径等穿越);关系库里没有这些字节,句柄由业务侧持有。部分上传媒体(upload_audio/video/image)则仍经 objects 通道登记成 media_objects 行。
  • 切换字节落点对上层无感知。 路由只依赖 Storage.objects / Storage.files 接口,localcos 是同接口的两套实现,由 OBJECT_STORE 装配;local 目录布局与 COS key 一一镜像,迁移即按相同 key 搬运。
  • 隔离在对象轴同样成立。 对象 key 的 scope 分 sys(TTS、情景音频等系统生成、内容寻址、跨用户共享)与 u{uid}(用户私有上传);关系行按 user_id 隔离,私有对象按 key 前缀隔离。
完整的 65 表存储拓扑、对象 key 三级结构与 COS 三步写入(先插占位行 → 上传 → 回填 key,避免桶里出现无记录的孤儿对象)见存储架构:关系型数据库与对象存储;COS 开通与运维见对象存储(COS)

初始化、迁移与种子数据

三个 schema SQL 是新数据库的事实来源(collection_server 无 schema 文件):
  • rakull_server/data_server/app/db/schema.sql(PostgreSQL 库 rakull_data
  • rakull_server/data_server/app/db/collection_schema.sql(PostgreSQL 库 rakull_collection
  • rakull_server/user_manager/app/db/schema.sql(SQLite 库 local_auth.db
服务 / 库bootstrap 与迁移职责
data_serverrakull_datarakull_collectionapp/db/bootstrap.pyinit_db() 对两个库分别执行 schema.sql / collection_schema.sql;存储工厂 get_storage() 启动时也会再跑一次幂等建表(CREATE TABLE/INDEX/TRIGGER IF NOT EXISTS),随后幂等导入情景与旅程夹具目录。没有运行时补列机制:旧 SQLite 时代的 PRAGMA table_info 补列和 _migrate_collection() 已删除,schema.sql 头部明确不携带历史增量迁移,只面向全新建库。PostgreSQL 外键始终强制,不需要 SQLite 那样的连接开关
旧 SQLite → PostgreSQL 一次性迁移停服并完成首次 bootstrap 后,在 data_server 目录运行 scripts/migrate_sqlite_to_pg.py --data-sqlite data.db --collection-sqlite collection.db [--replace](旧文件就在服务根目录):以只读方式打开旧库,按 PostgreSQL schema 中实际存在的表/列逐列复制(身份列用 OVERRIDING SYSTEM VALUE 并重置序列,加载期间禁用用户触发器、临时摘除外键),单库单事务、失败整体回滚;目标库已含种子行时需 --replace 先 TRUNCATE。#82 之前遗留在 ../collection_server/collection.db 的历史收藏可用 --legacy-collection-sqlite 按并集重排主键后一并迁入;完整步骤见安装与快速开始
user_managerlocal_auth.db仍是 SQLite:app/db/migrations.pyapply_migrations()PRAGMA table_info 检查并补充旧库缺少的 7 个 users 赞助字段;schema 用 INSERT OR IGNORE 预置邀请码;bootstrap 调用 app/db/seed.py 幂等写入哈希后的 root 管理员
改结构时直接改对应 schema SQL(data_server)或 schema + 旧库迁移(user_manager),不要把字段字典当可执行迁移。业务含义见领域模型与数据归属,写入顺序见文章处理的持久化流程