beego ORM 源码(上):模型元数据与注册

1. 三层文件地图

bash 复制代码
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)

go 复制代码
type ModelInfo struct {
    Manual bool                  // 用户显式注册(非 m2m 中间表)
    IsThrough bool               // rel_through 生成的直通模型
    Pkg, Name, FullName string   // 包路径/类型名/"pkg.Name"
    Table string                 // 表名
    Model interface{}            // 注册时的实例
    Fields *Fields               // 字段集合
    AddrField reflect.Value
    Uniques []string
}

Fields(models_info_f.go:29-41)

go 复制代码
type FieldInfo struct {
    DBcol bool          // 是否真实列(fk/one→true;m2m/reverse→false)
    Auto/Pk/Null/Index/Unique/InModel bool
    AutoNow/AutoNowAdd bool
    Rel bool            // rel(fk/one/m2m)
    Reverse bool        // reverse(one/many)
    IsFielder bool      // 实现 Fielder 自定义字段
    Mi *ModelInfo       // 所属模型
    FieldIndex []int    // 嵌入字段路径(反射 FieldByIndex 用)
    FieldType int
    Name, Column string
    AddrValue reflect.Value
    Initial interface{} // default 值
    Size, Digits, Decimals int
    RelModelInfo *ModelInfo        // 指向对方模型
    ReverseFieldInfo / ReverseFieldInfoTwo / ReverseFieldInfoM2M  // 反向引用
    RelTable, RelThrough string
    OnDelete string
    ...
}

索引全家桶:Pk *FieldInfoFields/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)

scss 复制代码
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)

scss 复制代码
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 最烧脑的一段,建议对照下图读:

css 复制代码
第一轮:为每个 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)

csharp 复制代码
type alias struct {
    Name, DriverName, DataSource string
    Driver DriverType
    DB *DB                    // 包装 *sql.DB + RWMutex
    DbBaser dbBaser            // 方言实现
    TZ *time.Location          // 探测的 DB 时区
    MaxIdleConns/MaxOpenConns/ConnMaxLifetime/ConnMaxLifetime/StmtCacheSize
}

dataBaseCache(58,_dbCache RWMutex 保护)全局别名表;drivers/dbBasers 两个 map 决定"驱动名→DriverType→dbBaser 实例"。

6.2 RegisterDataBase(463-492)

scss 复制代码
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)

scss 复制代码
getStmtDecorator(query):DCL(双检锁)查 LRU → 未命中 Prepare → 存入
stmtDecorator:sync.WaitGroup 引用计数,淘汰/关闭时等在用者退出

同一 SQL 反复执行只 Prepare 一次------高 QPS 下显著收益;LRU 淘汰的"等引用归零再关"是精细的并发设计。

7. Ormer 家族(orm.go)

scss 复制代码
ormBase{alias, db dbQuerier}(98-101)      // 公共基类:Read/Insert/...
orm  = ormBase + Begin                    // Ormer
txOrm = ormBase + Commit/Rollback(604-620)
newDBWithAlias(623-661):
    BootStrapWithAlias(首次)
    Debug → newDbQueryLog 包装 db(打印 SQL)
    globalFilterChains 非空 → NewFilterOrmDecorator(洋葱链,S13)

单对象 CRUD 的 SQL 生成在 dbBase(db.go),流程速记(细节见 S12):

sql 复制代码
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. 设计赏析

  1. 元数据一次构建:所有反射成本集中在 Bootstrap(Once);运行期全部走内存 FieldInfo------"启动慢一点,运行快到底"。
  1. fail-fast:tag 错误、重复注册直接 os.Exit------把开发期错误拦在部署前。
  1. 嵌入字段递归展开 + FieldIndex:Go 结构体嵌入语义完整映射到 ORM(组合复用模型的正道)。
  1. sync.Once 的边界:Bootstrap 只对"默认库"与"按别名"各做一次,测试里 ResetModelCache 配合重建。
相关推荐
索隆zoro1 小时前
Army 对 jOOQ
java·后端
泡海椒1 小时前
规则热更新实现:JQuick-Java无需重启更新业务规则实战
后端
右耳朵猫AI2 小时前
PHP周刊2026W37 | Symfony 三维护版齐发、Laravel AI SDK 0.11、LSP 服务器上线
后端·php·laravel
geovindu2 小时前
CSharp:Condition Variable Pattern
后端·设计模式·c#·.net·.netcore·条件变量模式·同步型模式
Java内核笔记2 小时前
Spring Boot 4 SSRF 防护源码剖析:InetAddressFilter 挡住内网地址与云元数据
java·后端
右耳朵猫AI2 小时前
Node.js周刊2026W37 | 三处进程崩溃修复、fs 内置 glob、Workers 模块注册表、Vitest 5.0
javascript·后端·node.js
giszhc2 小时前
腾讯地图瓦片接入方案:一个 Bun 代理,让 Mapbox / Leaflet / OpenLayers 通用
前端·后端
她的男孩2 小时前
加了 @Idempotent 还是重复扣款了 3 笔:扒完 1279 行幂等 Starter,我挖出 5 个隐蔽的坑
java·后端·架构
pe7er3 小时前
引子
前端·后端·架构