ARTICLE · 1018715
beego ORM 源码(上):模型元数据与注册
1. 三层文件地图
client/orm/├── models_boot.go # 对外薄封装:defaultModelCache、RegisterModel├── internal/models/ # 元数据核心(v2 移入 internal,防外部依赖)│ ├── models.go # ModelCache.Register、Bootstrap(sync.Once 四轮解析)│ ├── models_info_m.go # ModelInfo│ ├── models_info_f.go # FieldInfo/Fields、NewFieldInfo(tag 解析!)│ └── models_utils.go # snakeString、supportTag、类型推导├── db_alias.go # alias、dataBaseCache、RegisterDataBase、stmt 缓存├── orm.go # Ormer/txOrm 实现、DML/DQL├── orm_queryset.go # QuerySeter├── db.go # dbBase:SQL 生成与执行(方言无关主干)├── db_tables.go # JOIN 引擎(dbTables)├── db_mysql/postgres/sqlite/oracle/tidb.go # 方言 dbBaser└── types.go # 所有接口
2. 元数据结构
ModelInfo(models_info_m.go:24-35)
type ModelInfo struct {Manual bool // 用户显式注册(非 m2m 中间表)IsThrough bool // rel_through 生成的直通模型Pkg, Name, FullName string // 包路径/类型名/"pkg.Name"Table string // 表名Model interface{} // 注册时的实例Fields *Fields // 字段集合AddrField reflect.ValueUniques []string}
Fields(models_info_f.go:29-41)
type FieldInfo struct {DBcol bool // 是否真实列(fk/one→true;m2m/reverse→false)Auto/Pk/Null/Index/Unique/InModel boolAutoNow/AutoNowAdd boolRel bool // rel(fk/one/m2m)Reverse bool // reverse(one/many)IsFielder bool // 实现 Fielder 自定义字段Mi *ModelInfo // 所属模型FieldIndex []int // 嵌入字段路径(反射 FieldByIndex 用)FieldType intName, Column stringAddrValue reflect.ValueInitial interface{} // default 值Size, Digits, Decimals intRelModelInfo *ModelInfo // 指向对方模型ReverseFieldInfo / ReverseFieldInfoTwo / ReverseFieldInfoM2M // 反向引用RelTable, RelThrough stringOnDelete string...}
索引全家桶:Pk *FieldInfo、Fields/FieldsLow(字段名→FieldInfo)、FieldsByType(类型分组)、FieldsRel/FieldsReverse/FieldsDB(关系/反向/真实列)、Orders/DBcols(有序列名)。查找 GetByName/GetByColumn/GetByAny 三级回退。
FieldInfo(105-144,精简)
3. tag 解析:NewFieldInfo(models_info_f.go)
3.1 supportTag 白名单(models_utils.go:27-50)
属性型 8 个 + 键值型 13 个(完整表见使用篇 Day09)。ParseStructTag(237-256):按 ; 拆,识别 key(value) 语法;非法 tag 直接 os.Exit(2)(fail-fast,注册期问题绝不带到运行期)。
3.2 类型推导 getFieldType(158-234)
Go 类型 → FieldType 枚举(string→TypeVarCharField、time.Time→TypeDateTimeField、sql.NullInt64→TypeBigIntegerField…);指针自动解引用。
3.3 列名推导(141-155)
column(name) 指定 → 用之否则 SnakeString(字段名)fk/one 追加 "_id"m2m/reverse:不占列(字段名仅用于关系)
SnakeString(280-295):XxYy → xx_yy 大写前插下划线;NameStrategyMap 可切换缩略词策略(acronym)。
3.4 关系字段类型校验(278-294)
fk/one/reverse:one 必须 *Struct;m2m/reverse:many 必须 []*Struct;auto 仅整型且隐含 pk(427-440)。
4. RegisterModel(models.go:314-378)
RegisterModel(val):1. 反射校验:struct 指针(允许 slice 取元素),禁双重指针2. GetTableName:TableName() 方法 > TableEngine? > SnakeString(struct 名)+ 可选 prefix/suffix(RegisterModelWithPrefix/Suffix)3. 查重:FullName 重复、table 重复 → 报错4. NewModelInfo → AddModelFields(50-100):未导出字段跳过匿名(嵌入)字段 → 递归展开,FieldIndex 拼路径 ← 结构体嵌入的 ORM 语义orm:"-" 跳过列名冲突/多主键 → os.Exit(2)5. 无 PK 时:自动找名为 "id" 的整型字段设 Auto+Pk(356-368)
5. Bootstrap:四轮关系解析(models.go:103-311,sync.Once)
NewOrm 首次触发(BootStrapWithAlias)。这是 ORM 最烧脑的一段,建议对照下图读:
第一轮:为每个 rel/reverse 字段挂上 RelModelInfo(按 FullName 查 ModelCache)m2m 且无 rel_through → NewM2MModelInfo(m1,m2):生成中间表 ModelInfo:表名 "m1_m2s",字段 = 自增 id + m1_id + m2_id标记 IsThrough(104-148)第二轮:为每个 fk/one/m2m 在对方模型"补" reverse 字段反向字段名 = 模型名小写;冲突重试 Name0..Name4第三轮:m2m 有 through 时:在 through 模型里定位 ReverseFieldInfoTwo(连接"我→through"与"through→对方"的两条边)第四轮:回填 reverse:one → ReverseFieldInfo;reverse:many(源自 m2m)→ ReverseFieldInfoM2M形成双向链:User.Roles ↔ Role.Users
至此,关系图在内存元数据里闭环;QuerySeter 的跨表 Filter(Posts__Title__contains)正是沿着这张图走。
6. 别名与连接:db_alias.go
6.1 alias(284-298)
type alias struct {Name, DriverName, DataSource stringDriver DriverTypeDB *DB // 包装 *sql.DB + RWMutexDbBaser dbBaser // 方言实现TZ *time.Location // 探测的 DB 时区MaxIdleConns/MaxOpenConns/ConnMaxLifetime/ConnMaxLifetime/StmtCacheSize}
dataBaseCache(58,_dbCache RWMutex 保护)全局别名表;drivers/dbBasers 两个 map 决定"驱动名→DriverType→dbBaser 实例"。
6.2 RegisterDataBase(463-492)
sql.Open(懒连接)→ addAliasWthDB → newAliasWithDb(374-418)应用 DOption(MaxIdle/MaxOpen/Lifetime/StmtCache)StmtCacheSize>0 → lru.NewWithEvict(553-575)淘汰时:stmtDecorator.destroy() 等 WaitGroup 清零再 Close(并发安全关 stmt!)dbBasers[driverType] 挂方言db.Ping() 校验detectTZ(300-354):MySQL: SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP()) + 探默认引擎postgres: SELECT current_setting('TIMEZONE')sqlite/oracle: 固定 UTC存入 dataBaseCache
6.3 语句缓存(DB.ExecContext 等,172-220)
getStmtDecorator(query):DCL(双检锁)查 LRU → 未命中 Prepare → 存入stmtDecorator:sync.WaitGroup 引用计数,淘汰/关闭时等在用者退出
同一 SQL 反复执行只 Prepare 一次——高 QPS 下显著收益;LRU 淘汰的"等引用归零再关"是精细的并发设计。
7. Ormer 家族(orm.go)
ormBase{alias, db dbQuerier}(98-101) // 公共基类:Read/Insert/...orm = ormBase + Begin // OrmertxOrm = ormBase + Commit/Rollback(604-620)newDBWithAlias(623-661):BootStrapWithAlias(首次)Debug → newDbQueryLog 包装 db(打印 SQL)globalFilterChains 非空 → NewFilterOrmDecorator(洋葱链,S13)
单对象 CRUD 的 SQL 生成在 dbBase(db.go),流程速记(细节见 S12):
Read: getExistPk → SELECT ... WHERE pk=? → convertValueFromDB 回填Insert: collectValues → INSERT ... → LastInsertId → setPk 回写自增Update: 全 DBcols(剔 AutoNowAdd,补 AutoNow=now)Delete: DELETE WHERE pk → deleteRels(on_delete 级联:cascade/set_null/set_default/do_nothing)
8. 设计赏析
- 元数据一次构建:所有反射成本集中在 Bootstrap(Once);运行期全部走内存 FieldInfo——"启动慢一点,运行快到底"。
- fail-fast:tag 错误、重复注册直接 os.Exit——把开发期错误拦在部署前。
- 嵌入字段递归展开 + FieldIndex:Go 结构体嵌入语义完整映射到 ORM(组合复用模型的正道)。
- sync.Once 的边界:Bootstrap 只对"默认库"与"按别名"各做一次,测试里 ResetModelCache 配合重建。