Gogs 数据库表结构全解:八张核心表在 PostgreSQL / MySQL / SQLite3 下的设计与源码映射
2026/9/8 21:16:50 网站建设 项目流程

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所定义的八张表中:accessaccess_tokenactionemail_addressfollowlfs_objectlogin_sourcenotice。本文以该文档中各表的跨数据库字段定义为骨架,结合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.oidlogin_source.name例外,MySQL 下为VARCHAR(191),这通常是为了兼容旧版本索引前缀长度限制)、SQLite3 用TEXT
  • 布尔值:PostgreSQL/MySQL 为BOOLEAN,SQLite3 以NUMERIC表示;
  • 时间戳:多数表存 Unix 秒(created_unix),仅lfs_object例外,存created_at时间类型。

二、access:仓库权限表

文档定义:

FieldColumnPostgreSQLMySQLSQLite3
IDidBIGSERIALBIGINT AUTO_INCREMENTINTEGER AUTOINCREMENT
UserIDuser_idBIGINT NOT NULLBIGINT NOT NULLINTEGER NOT NULL
RepoIDrepo_idBIGINT NOT NULLBIGINT NOT NULLINTEGER NOT NULL
ModemodeBIGINT NOT NULLBIGINT NOT NULLINTEGER NOT NULL

主键为id,唯一索引access_user_repo_unique (user_id, repo_id)保证同一用户对同一仓库至多一条权限记录。

Mode列的取值由源码中的枚举 AccessMode 定义:0=none1=read2=write3=admin4=owner,对应文档中该列的整型存储。Access 结构体 通过gorm:"uniqueIndex:access_user_repo_unique;not null"标签精确复现了文档中的唯一索引名。

这张表有一个容易被忽略的语义细节:结构体注释明确说明,仓库的真实所有者并不存入该表;只有组织仓库中 owners 团队成员、协作者等"非所有者"权限才记录于此。权限判定的完整链路见 AccessMode 方法:公开仓库对所有人先给读权限 → 匿名到此为止 → 命中所有者 ID 直接返回 Owner → 最后按user_id + repo_id查表。批量授权走 SetRepoPerms,在一个事务里先删该仓库全部旧记录再整批重建。排查"某用户为什么能/不能 push"时,这条判定顺序就是依据。

三、access_token:个人访问令牌表

文档定义:

FieldColumnPostgreSQLMySQLSQLite3
IDidBIGSERIALBIGINT AUTO_INCREMENTINTEGER AUTOINCREMENT
UserIDuidBIGINTBIGINTINTEGER
NamenameTEXTLONGTEXTTEXT
Sha1sha1VARCHAR(40) UNIQUEVARCHAR(40) UNIQUEVARCHAR(40) UNIQUE
SHA256sha256VARCHAR(64) NOT NULL UNIQUE(三库一致)
CreatedUnix / UpdatedUnixcreated_unix / updated_unixBIGINT(三库对应整型)

主键id,普通索引idx_access_token_user_id (uid)。注意两列列名细节:UserID落库列为uid而非user_id,由 AccessToken 结构体 的gorm:"column:uid;index"标签保证;SHA256列在 GORM 中的大写字段名会按标签映射为小写sha256列。

这张表是版本迁移机制的典型案例。SHA256列是后加的——迁移 migrateAccessTokenToSHA256 完整展示了"给存量表加带约束新列"的三步事务操作:

  1. 先不加约束地AddColumn(此时所有行均为 NULL,无法直接上NOT NULL);
  2. 逐行取sha256 IS NULL的记录,用 cryptox.SHA256 对旧sha1值做二次摘要回填;
  3. 数据补齐后,再通过带unique;not null约束的结构体执行AutoMigrate,安全地施加约束。

sha1列保留为兼容历史 token 的查询通道,sha256列则承担当前认证路径。另外,AfterFind钩子(access_tokens.go#L41-L49)会从updated_unix派生HasUsed和"近 7 天有活动"两个瞬态字段,用于 UI 上提示闲置令牌,这些字段不落库(gorm:"-")。

四、action:用户操作日志表

文档定义(字段较多,完整列出):

FieldColumnPostgreSQLMySQLSQLite3
IDidBIGSERIALBIGINT AUTO_INCREMENTINTEGER AUTOINCREMENT
UserIDuser_idBIGINTBIGINTINTEGER
OpTypeop_typeBIGINTBIGINTINTEGER
ActUserIDact_user_idBIGINTBIGINTINTEGER
ActUserNameact_user_nameTEXTLONGTEXTTEXT
RepoIDrepo_idBIGINTBIGINTINTEGER
RepoUserNamerepo_user_nameTEXTLONGTEXTTEXT
RepoNamerepo_nameTEXTLONGTEXTTEXT
RefNameref_nameTEXTLONGTEXTTEXT
IsPrivateis_privateBOOLEAN NOT NULL DEFAULT FALSE(SQLite3 为 NUMERIC)
ContentcontentTEXTLONGTEXTTEXT
CreatedUnixcreated_unixBIGINTBIGINTINTEGER

主键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文档定义:

FieldColumnPostgreSQLMySQLSQLite3
IDidBIGSERIALBIGINT AUTO_INCREMENTINTEGER AUTOINCREMENT
UserIDuidBIGINT NOT NULLBIGINT NOT NULLINTEGER NOT NULL
EmailemailVARCHAR(254) NOT NULLVARCHAR(254) NOT NULLTEXT NOT NULL
IsActivatedis_activatedBOOLEAN 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_idfollow_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 对象归属表

文档定义是八张表中结构最特殊的一张:

FieldColumnPostgreSQLMySQLSQLite3
RepoIDrepo_idBIGINTBIGINTINTEGER
OIDoidTEXTVARCHAR(191)TEXT
SizesizeBIGINT NOT NULLBIGINT NOT NULLINTEGER NOT NULL
StoragestorageTEXT NOT NULLLONGTEXT NOT NULLTEXT NOT NULL
CreatedAtcreated_atTIMESTAMPTZ NOT NULLDATETIME(3) NOT NULLDATETIME NOT NULL

主键为复合主键(repo_id, oid),且没有自增 ID 列。它记录"某个 LFS 对象归属于哪个仓库",配合internal/lfsx的对象存储层使用:LFSObject 结构体 中RepoID声明auto_increment:false并参与复合主键,OID使用强校验的lfsx.OID类型。写入入口 CreateObject 在对象成功落盘后登记该关系。由于主键即业务键,同一仓库下同一 OID 天然幂等,重复推送同一 LFS 对象不会产生脏记录。

七、login_source:登录源配置表

文档定义:

FieldColumnPostgreSQLMySQLSQLite3
IDidBIGSERIALBIGINT AUTO_INCREMENTINTEGER AUTOINCREMENT
TypetypeBIGINTBIGINTINTEGER
NamenameTEXT UNIQUEVARCHAR(191) UNIQUETEXT UNIQUE
IsActivedis_activedBOOLEAN NOT NULLBOOLEAN NOT NULLNUMERIC NOT NULL
IsDefaultis_defaultBOOLEANBOOLEANNUMERIC
ConfigcfgTEXTTEXTTEXT
CreatedUnix / UpdatedUnixcreated_unix / updated_unixBIGINT

主键id,无额外索引(name的 UNIQUE 约束自带索引)。模型 LoginSource 中Config落库列为cfggorm:"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),TrStradmin.notices.type_N生成翻译键,说明该表是面向管理员后台的系统级提示(例如定时清理任务失败的兜底通知,见同文件的RemoveAllWithNotice)。

九、版本迁移机制:schema 演进的落点

上述各表中的"后加列"(如access_token.sha256)与"后加索引"(如action.user_id)都不是靠启动时全量AutoMigrate实现的,而是沉淀在 migrations.go 的有序迁移列表中。该文件的机制值得完整理解:

  • 版本锚点minDBVersion = 19version表仅存一行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行定位卡在哪一条。

十、跨数据库差异速查与实操建议

把文档三列类型差异归纳为四条实操规则:

  1. 布尔列:SQLite3 一律是NUMERIC,手写 SQL 判空用= 1/= 0最稳妥;
  2. 唯一索引命名:GORM 标签显式指定了索引名(access_user_repo_uniqueemail_address_user_email_uniquefollow_user_follow_unique),跨数据库查表结构时可直接用这些名字EXPLAIN验证索引是否命中;
  3. MySQL 变长文本:除lfs_object.oidlogin_source.nameVARCHAR(191))外统一LONGTEXT,若需手工建兼容表注意区分;
  4. 时间列:除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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询