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 *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)
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. 设计赏析
- 元数据一次构建:所有反射成本集中在 Bootstrap(Once);运行期全部走内存 FieldInfo------"启动慢一点,运行快到底"。
- fail-fast:tag 错误、重复注册直接 os.Exit------把开发期错误拦在部署前。
- 嵌入字段递归展开 + FieldIndex:Go 结构体嵌入语义完整映射到 ORM(组合复用模型的正道)。
- sync.Once 的边界:Bootstrap 只对"默认库"与"按别名"各做一次,测试里 ResetModelCache 配合重建。