Gogs 数据库表结构全解:八张核心表在 PostgreSQL / MySQL / SQLite3 下的设计与源码映射
【免费下载链接】gogsThe painless way to host your own Git service项目地址: https://gitcode.com/GitHub_Trending/go/gogs
Gogs 采用"少表、多复用"的数据库设计,全部核心数据被收敛在docs/dev/database_schema.md所定义的八张表中:access、access_token、action、email_address、follow、lfs_object、login_source、notice。本文以该文档中各表的跨数据库字段定义为骨架,结合internal/database下的模型结构与internal/database/migrations中的版本迁移实现,逐表解读每张表的字段语义、索引策略、GORM 标签映射以及数据迁移时的关键细节,帮助你在排查权限问题、审计日志、LFS 对象归属或升级故障时,能直接从表结构定位到源码实现。
一、总体设计:八张表与三数据库类型兼容
docs/dev/database_schema.md中每张表都以三列对照的形式给出字段定义:PostgreSQL 一列、MySQL 一列、SQLite3 一列,这正是 Gogs 支持三种数据库后端的直接体现。与文档一一对应的是源码中的建表清单 Tables,它以字母序排列、逐行独立列出八个模型结构体:
// Tables is the list of struct-to-table mappings. var Tables = []any{ new(Access), new(AccessToken), new(Action), new(EmailAddress), new(Follow), new(LFSObject), new(LoginSource), new(Notice), }建表行为发生在 NewConnection 中:GORM 配置了NamingStrategy{SingularTable: true}(表名用单数形式),MySQL 下显式指定ENGINE=InnoDB。值得注意的是其中一条注释:"only use it to create new tables, and do customize migration with future changes"——即AutoMigrate仅用于不存在的表(HasTable检测后跳过已有表),后续 schema 变更一律走独立的自定义迁移逻辑。这解释了为什么文档中记录的 schema 是"稳定态",而新增列、加索引这类改动都沉淀在迁移文件里(详见后文"版本迁移机制"一节)。
从源码结构看,各表的字段类型遵循一套统一约定,理解它之后即可读懂文档中三列类型对照的含义:
- 自增主键:PostgreSQL 用
BIGSERIAL,MySQL 用BIGINT AUTO_INCREMENT,SQLite3 用INTEGER AUTOINCREMENT,在 GORM 中统一映射为int64+gorm:"primaryKey"; - 外键式关联(用户 ID、仓库 ID):统一为各数据库的大整数类型(
BIGINT/INTEGER),不依赖数据库外键约束,关联完整性由应用层保证; - 文本:PostgreSQL 用
TEXT、MySQL 用LONGTEXT(注意lfs_object.oid和login_source.name例外,MySQL 下为VARCHAR(191),这通常是为了兼容旧版本索引前缀长度限制)、SQLite3 用TEXT; - 布尔值:PostgreSQL/MySQL 为
BOOLEAN,SQLite3 以NUMERIC表示; - 时间戳:多数表存 Unix 秒(
created_unix),仅lfs_object例外,存created_at时间类型。
二、access:仓库权限表
文档定义:
| Field | Column | PostgreSQL | MySQL | SQLite3 |
|---|---|---|---|---|
| ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER AUTOINCREMENT |
| UserID | user_id | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL |
| RepoID | repo_id | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL |
| Mode | mode | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL |
主键为id,唯一索引access_user_repo_unique (user_id, repo_id)保证同一用户对同一仓库至多一条权限记录。
Mode列的取值由源码中的枚举 AccessMode 定义:0=none、1=read、2=write、3=admin、4=owner,对应文档中该列的整型存储。Access 结构体 通过gorm:"uniqueIndex:access_user_repo_unique;not null"标签精确复现了文档中的唯一索引名。
这张表有一个容易被忽略的语义细节:结构体注释明确说明,仓库的真实所有者并不存入该表;只有组织仓库中 owners 团队成员、协作者等"非所有者"权限才记录于此。权限判定的完整链路见 AccessMode 方法:公开仓库对所有人先给读权限 → 匿名到此为止 → 命中所有者 ID 直接返回 Owner → 最后按user_id + repo_id查表。批量授权走 SetRepoPerms,在一个事务里先删该仓库全部旧记录再整批重建。排查"某用户为什么能/不能 push"时,这条判定顺序就是依据。
三、access_token:个人访问令牌表
文档定义:
| Field | Column | PostgreSQL | MySQL | SQLite3 |
|---|---|---|---|---|
| ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER AUTOINCREMENT |
| UserID | uid | BIGINT | BIGINT | INTEGER |
| Name | name | TEXT | LONGTEXT | TEXT |
| Sha1 | sha1 | VARCHAR(40) UNIQUE | VARCHAR(40) UNIQUE | VARCHAR(40) UNIQUE |
| SHA256 | sha256 | VARCHAR(64) NOT NULL UNIQUE(三库一致) | ||
| CreatedUnix / UpdatedUnix | created_unix / updated_unix | BIGINT(三库对应整型) |
主键id,普通索引idx_access_token_user_id (uid)。注意两列列名细节:UserID落库列为uid而非user_id,由 AccessToken 结构体 的gorm:"column:uid;index"标签保证;SHA256列在 GORM 中的大写字段名会按标签映射为小写sha256列。
这张表是版本迁移机制的典型案例。SHA256列是后加的——迁移 migrateAccessTokenToSHA256 完整展示了"给存量表加带约束新列"的三步事务操作:
- 先不加约束地
AddColumn(此时所有行均为 NULL,无法直接上NOT NULL); - 逐行取
sha256 IS NULL的记录,用 cryptox.SHA256 对旧sha1值做二次摘要回填; - 数据补齐后,再通过带
unique;not null约束的结构体执行AutoMigrate,安全地施加约束。
sha1列保留为兼容历史 token 的查询通道,sha256列则承担当前认证路径。另外,AfterFind钩子(access_tokens.go#L41-L49)会从updated_unix派生HasUsed和"近 7 天有活动"两个瞬态字段,用于 UI 上提示闲置令牌,这些字段不落库(gorm:"-")。
四、action:用户操作日志表
文档定义(字段较多,完整列出):
| Field | Column | PostgreSQL | MySQL | SQLite3 |
|---|---|---|---|---|
| ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER AUTOINCREMENT |
| UserID | user_id | BIGINT | BIGINT | INTEGER |
| OpType | op_type | BIGINT | BIGINT | INTEGER |
| ActUserID | act_user_id | BIGINT | BIGINT | INTEGER |
| ActUserName | act_user_name | TEXT | LONGTEXT | TEXT |
| RepoID | repo_id | BIGINT | BIGINT | INTEGER |
| RepoUserName | repo_user_name | TEXT | LONGTEXT | TEXT |
| RepoName | repo_name | TEXT | LONGTEXT | TEXT |
| RefName | ref_name | TEXT | LONGTEXT | TEXT |
| IsPrivate | is_private | BOOLEAN NOT NULL DEFAULT FALSE(SQLite3 为 NUMERIC) | ||
| Content | content | TEXT | LONGTEXT | TEXT |
| CreatedUnix | created_unix | BIGINT | BIGINT | INTEGER |
主键id,索引idx_action_repo_id (repo_id)与idx_action_user_id (user_id)。
两个索引的来历值得对照源码:repo_id索引由 Action 结构体 上gorm:"index"标签声明,而user_id索引是后来补的——迁移 addIndexToActionUserID 先HasIndex探测、已存在则跳过,再CreateIndex。这说明该表查询热点在"按仓库翻动态"和"按用户查收件"两条路径上。
字段语义上有三对易混淆概念,读表前应分清:
user_id是动态的接收者(订阅了动态的用户),act_user_id/act_user_name是操作者(doer),仓库归属信息则冗余存在repo_user_name/repo_name中,避免联表;op_type覆盖推送、创建/关闭 issue 与 pull request、分支标签增删、fork、mirror 同步等二十余种操作类型(见 actions.go#L700-L709 的枚举尾部);content存操作详情(如 issue 编号、分支名),is_private标记该动态所属仓库是否私有,用于动态页的可见性过滤。
结构体上的BeforeCreate/AfterFind钩子(actions.go#L731-L743)解释了created_unix的使用方式:写入时若未显式赋值则自动取当前 Unix 秒,读取时再换算回time.Time供模板渲染——这是 Gogs 时间存储的通用模式(lfs_object是唯一例外)。
五、email_address 与 follow:用户关联表
email_address文档定义:
| Field | Column | PostgreSQL | MySQL | SQLite3 |
|---|---|---|---|---|
| ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER AUTOINCREMENT |
| UserID | uid | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL |
| VARCHAR(254) NOT NULL | VARCHAR(254) NOT NULL | TEXT NOT NULL | ||
| IsActivated | is_activated | BOOLEAN NOT NULL DEFAULT FALSE(SQLite3 为 NUMERIC) |
主键id;联合唯一索引email_address_user_email_unique (uid, email)外加uid单列索引idx_email_address_user_id。模型 EmailAddress 中email列显式size:254(与文档的VARCHAR(254)一致),IsPrimary是瞬态字段不落库。
follow文档定义:id(自增主键)、user_id、follow_id,三列均 NOT NULL,唯一索引follow_user_follow_unique (user_id, follow_id)防止重复关注。模型 Follow 用uniqueIndex:follow_user_follow_unique复现该约束。这张表是标准的"关系表":(user_id, follow_id)一行即"user_id 关注了 follow_id",双向查询(粉丝/关注列表)各走该索引的一个方向。
六、lfs_object:LFS 对象归属表
文档定义是八张表中结构最特殊的一张:
| Field | Column | PostgreSQL | MySQL | SQLite3 |
|---|---|---|---|---|
| RepoID | repo_id | BIGINT | BIGINT | INTEGER |
| OID | oid | TEXT | VARCHAR(191) | TEXT |
| Size | size | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL |
| Storage | storage | TEXT NOT NULL | LONGTEXT NOT NULL | TEXT NOT NULL |
| CreatedAt | created_at | TIMESTAMPTZ NOT NULL | DATETIME(3) NOT NULL | DATETIME NOT NULL |
主键为复合主键(repo_id, oid),且没有自增 ID 列。它记录"某个 LFS 对象归属于哪个仓库",配合internal/lfsx的对象存储层使用:LFSObject 结构体 中RepoID声明auto_increment:false并参与复合主键,OID使用强校验的lfsx.OID类型。写入入口 CreateObject 在对象成功落盘后登记该关系。由于主键即业务键,同一仓库下同一 OID 天然幂等,重复推送同一 LFS 对象不会产生脏记录。
七、login_source:登录源配置表
文档定义:
| Field | Column | PostgreSQL | MySQL | SQLite3 |
|---|---|---|---|---|
| ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER AUTOINCREMENT |
| Type | type | BIGINT | BIGINT | INTEGER |
| Name | name | TEXT UNIQUE | VARCHAR(191) UNIQUE | TEXT UNIQUE |
| IsActived | is_actived | BOOLEAN NOT NULL | BOOLEAN NOT NULL | NUMERIC NOT NULL |
| IsDefault | is_default | BOOLEAN | BOOLEAN | NUMERIC |
| Config | cfg | TEXT | TEXT | TEXT |
| CreatedUnix / UpdatedUnix | created_unix / updated_unix | BIGINT |
主键id,无额外索引(name的 UNIQUE 约束自带索引)。模型 LoginSource 中Config落库列为cfg(gorm:"column:cfg"),存的是各 Provider 的 JSON 配置;Provider字段是运行时反序列化出来的gorm:"-"瞬态对象。type列区分 PAM、LDAP、SMTP、GitHub 等登录方式,与conf/auth.d/下的示例配置(如 github.conf.example、pam.conf.example)共同构成管理员后台配置登录源时的取值参考。
八、notice:系统通知表
文档定义:id(自增主键)、type(BIGINT)、description(TEXT)、created_unix(BIGINT),无附加索引。模型 Notice 中当前只定义了NoticeTypeRepository = 1一种类型(notices.go#L63-L65),TrStr按admin.notices.type_N生成翻译键,说明该表是面向管理员后台的系统级提示(例如定时清理任务失败的兜底通知,见同文件的RemoveAllWithNotice)。
九、版本迁移机制:schema 演进的落点
上述各表中的"后加列"(如access_token.sha256)与"后加索引"(如action.user_id)都不是靠启动时全量AutoMigrate实现的,而是沉淀在 migrations.go 的有序迁移列表中。该文件的机制值得完整理解:
- 版本锚点:
minDBVersion = 19,version表仅存一行id = 1的版本记录(Version 结构体); - 迁移序列(migrations.go#L42-L62):
v19 → v20令牌 SHA256 迁移 →v20 → v21action 加索引 →v21 → v22是一个 noop(源码注释解释了它存在的原因:曾有一处版本计算 bug 导致部分实例停留在 v21、部分在 v22,noop 保证两条路径汇合后都能命中后续真实迁移); - 执行逻辑(Migrate):先保证
version表存在;新库直接写入minDBVersion + len(migrations)作为当前版本;旧库若版本低于minDBVersion则打印分版本回迁指引并拒绝继续(注释给出了 0.7.33 / 0.9.141 / 0.12.0 三个"最后支持版本"的对应关系);检测到降级部署时把版本号回调到可执行上限;否则从version - minDBVersion处逐条执行、每执行一条立即持久化版本号,单条迁移可返回errMigrationSkipped幂等跳过。
升级 Gogs 后如遇数据库结构报错,正确排查顺序是:先看version.version是否与minDBVersion + 迁移数匹配,再对照迁移日志中Migration: xxx行定位卡在哪一条。
十、跨数据库差异速查与实操建议
把文档三列类型差异归纳为四条实操规则:
- 布尔列:SQLite3 一律是
NUMERIC,手写 SQL 判空用= 1/= 0最稳妥; - 唯一索引命名:GORM 标签显式指定了索引名(
access_user_repo_unique、email_address_user_email_unique、follow_user_follow_unique),跨数据库查表结构时可直接用这些名字EXPLAIN验证索引是否命中; - MySQL 变长文本:除
lfs_object.oid、login_source.name(VARCHAR(191))外统一LONGTEXT,若需手工建兼容表注意区分; - 时间列:除
lfs_object.created_at(时间类型)与 MySQL 的DATETIME(3)精度外,其余表统一 Unix 秒整数,跨表按时间过滤时应先确认目标表属于哪种。
结合 NewConnection 中按conf.Database.Type分支设置UsePostgreSQL/UseMySQL/UseSQLite3的逻辑可以确认:三种后端在应用层是同一套 GORM 模型,数据库特性差异被压缩到类型映射与gorm:table_options这一层,这也是文档能以一张三列表完整描述全部 schema 的前提。
小结
docs/dev/database_schema.md定义的八张表各自承担明确职责:access+follow构成权限与社交关系,action承载操作审计,access_token支撑 API 认证,email_address管理用户邮箱,lfs_object记录 LFS 对象归属,login_source保存外部登录源配置,notice存储系统级提示。所有字段与 Tables 列表 中的 GORM 模型一一对应,schema 演进则集中在 internal/database/migrations 的版本化迁移中完成。理解这套结构后,无论是调试权限判定、排查令牌认证失败,还是处理升级过程中的迁移报错,都可以从表定义直接走到对应的模型方法与迁移函数。
【免费下载链接】gogsThe painless way to host your own Git service项目地址: https://gitcode.com/GitHub_Trending/go/gogs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考