五、复杂查询、Protobuf 与代码生成

2021-02-15T14:21:02+08:00 | 27分钟阅读 | 更新于 2021-02-15T14:21:02+08:00

@

学习目标

学完本章你应该能够:

  1. 讲清 ORM 为什么需要 TableReference 抽象来支持 JOIN 与子查询,能画出 Table / Join / Subquery 三种实现的关系。
  2. 对比 INNER / LEFT / RIGHT JOIN 的语义差异与执行原理(近似双重循环),能说清不匹配行各自怎么处理。
  3. 解释 GORM Preload 的本质是 JOIN,并讲清 WHERE / ON / HAVING 的取舍——为什么建议把 JOIN 条件放 ON 上。
  4. 说明 Protobuf 生成代码无法携带 orm tag 的原因,讲清"修改插件源码"这一侵入式方案的来龙去脉与局限。
  5. 描述 AST 代码生成的完整链路(parse → inspect → template → 生成文件),并讲清它和 Java 动态代理的本质区别。

前置知识

  • 前四章 ORM 基础:Selector 查询、元数据(反射 + 缓存)、事务、AOP 中间件。
  • Go 接口、fmt.Sprintf 拼字符串、Builder 模式。
  • Protobuf 基本用法(.proto 写 message、protoc 生成 Go 代码)。

本章你会动手做的事

  1. orm.TableOf / Join / SubqueryOf 拼一个"嵌套 JOIN + 子查询",打印出生成的 SQL。
  2. 跑一遍文中的 AST 示例,把你自己的 model 结构体变成 user_gen.go
  3. 把 GORM 的 Preload 改写成显式 JOIN,对比二者生成的 SQL 是否一致。

一、复杂查询 —— JOIN 与子查询

到目前为止,我们的 ORM 已经支持了基本的增删改查、事务、AOP 等功能。但在实际业务中,单表查询往往不够用——我们需要 JOIN(联表查询)和子查询。这一章我们就来解决复杂查询的问题。

1.1 SQL JOIN 语法分析

JOIN 查询有点像我们的 Expression——可以查询套查询,无限套下去。让我们先看看不同数据库的 JOIN 语法。

类比:JOIN 就像查两本通讯录。你有一本"用户录"(user),一本"订单录"(order)。INNER JOIN 是只保留两本里都出现的人;LEFT JOIN 是左边全保留,右边没匹配到的空着(NULL);RIGHT JOIN 反过来。而"FROM 后面可以套查询",就像你可以先临时拼出一本小册子(子查询),再拿它去和别册 JOIN。

下面这张图展示了 TableReference 的三种实现,以及它们如何互相嵌套——这正是我们 ORM 要抽象的核心:

flowchart LR
    T[TableReference 接口] --> A[Table 普通表]
    T --> B[Join 连接查询]
    T --> C[Subquery 子查询]
    B -->|left 可以是| T
    C -->|内部持有| SEL[Selector 子查询]

MySQL JOIN 语法

-- INNER JOIN(内连接):只返回两表匹配的行
SELECT * FROM user u
INNER JOIN order o ON u.id = o.user_id;

-- LEFT JOIN(左连接):返回左表所有行,右表不匹配的为 NULL
SELECT * FROM user u
LEFT JOIN order o ON u.id = o.user_id;

-- RIGHT JOIN(右连接):返回右表所有行,左表不匹配的为 NULL
SELECT * FROM user u
RIGHT JOIN order o ON u.id = o.user_id;

-- USING 语法:当两表列名相同时可以用 USING
SELECT * FROM user u
INNER JOIN order o USING (user_id);

SQLite 和 PostgreSQL

SQLite 和 PostgreSQL 的 JOIN 语法与 MySQL 基本一致,都支持 INNER JOINLEFT JOINRIGHT JOIN,以及 ONUSING 两种条件语法。

1.2 JOIN 语法总结

JOIN 语法有两种形态:
├── JOIN ... ON       ← ON 后面跟条件表达式(如 u.id = o.user_id)
└── JOIN ... USING    ← USING 后面跟列名(如 USING(user_id))

JOIN 本身有:
├── INNER JOIN / JOIN   ← 内连接,只返回匹配的行
├── LEFT JOIN           ← 左连接,左表全保留
└── RIGHT JOIN          ← 右连接,右表全保留

FROM 后面可以是:
├── 普通的表            ← SELECT * FROM user
└── JOIN 查询           ← SELECT * FROM user JOIN order ON ...
    └── JOIN 可以嵌套    ← ... JOIN order ON ... JOIN item ON ...

1.3 主流 ORM 的 JOIN 设计

Beego ORM

Beego 的 JOIN 查询主要出现在两个地方:

  • QueryBuilder:提供了 InnerJoinLeftJoinRightJoin 三个方法
  • 关联关系:Beego 支持一对一、一对多、多对多关联,设置正确后会自动生成 JOIN 查询
// Beego 的 QueryBuilder JOIN
qb, _ := orm.NewQueryBuilder("mysql")
qb.Select("u.name", "o.id").
   From("user", "u").
   InnerJoin("order", "o").On("u.id = o.user_id").
   Where("u.age > ?", 18)
sql := qb.String()

观点:关联关系不是 ORM 框架应该处理的,而应该是领域对象(Repository 层面)处理的。ORM 只需要提供 JOIN 的能力,怎么用是用户的自由。

GORM

GORM 中 JOIN 查询主要是为了 Preload(预加载)服务:

// GORM 的 Preload
// 本质上是一个 JOIN 查询,把相关数据都查出来并组装成结构体
db.Preload("Orders").Find(&users)
// 会生成类似:SELECT * FROM users JOIN orders ON ...

面试要点:GORM 的 Preload 本质上就是一个 JOIN 查询。严格来说,在 Go 语言里面很难实现 lazy load(延迟加载),所以 GORM 的 Preload 就是通过 Join 把相关数据一次性查询出来并组装成结构体。

1.4 TableReference 抽象

为了支持 JOIN 和子查询,我们需要一个抽象——TableReference。FROM 后面不再只能接表名,而是可以接任何 TableReference

// table.go 文件

// TableReference 是 FROM 子句的抽象接口
// 它有三种实现:
//   1. Table:普通的表
//   2. Join:JOIN 查询
//   3. Subquery:子查询
type TableReference interface {
    // tableReference 是一个标记方法
    // 只有实现了此接口的类型才能用于 FROM 子句
    tableReference()
}

// ========== Table 普通表 ==========
// Table 代表一个普通的表
// 它也是 JOIN 查询的起点

type Table struct {
    // name 表名
    name  string
    // alias 表别名(AS)
    // 例如:FROM user u 中的 u
    alias string
}

// TableOf 创建一个 Table 实例
// 用法:orm.TableOf("user") 或 orm.TableOf("user", "u")
func TableOf(name string, alias ...string) *Table {
    t := &Table{name: name}
    if len(alias) > 0 {
        t.alias = alias[0]
    }
    return t
}

// tableReference 实现 TableReference 接口
func (t *Table) tableReference() {}

// As 设置表别名
// 返回新对象,支持链式调用
func (t *Table) As(alias string) *Table {
    return &Table{
        name:  t.name,
        alias: alias,
    }
}

1.5 JoinBuilder 和 Join 定义

JOIN 的设计理念与 OnDuplicateKeyBuilder 一致——使用 Builder 模式,OnUsing 是终结方法,直接返回 Join

// join.go 文件

// Join 代表一个 JOIN 查询
// JOIN 可以进一步 JOIN,形成嵌套
type Join struct {
    // typ JOIN 类型:"INNER"、"LEFT"、"RIGHT"
    typ    string
    // table 被连接的表
    table  TableReference
    // using USING 子句的列名列表(和 cond 互斥)
    using  []string
    // cond ON 子句的条件表达式(和 using 互斥)
    cond   Expression
    // left JOIN 的左操作数(用于嵌套 JOIN)
    left   TableReference
}

// tableReference 实现 TableReference 接口
// 这样 Join 本身也可以作为 FROM 的输入
func (j *Join) tableReference() {}

// ========== JoinBuilder ==========
// JoinBuilder 用于构建 JOIN 子句
// 设计理念与 OnDuplicateKeyBuilder 一致

type JoinBuilder struct {
    // typ JOIN 类型
    typ   string
    // table 被连接的表
    table TableReference
}

// Join 创建一个 JoinBuilder
// 用法:orm.Join(orm.TableOf("order", "o"))
func Join(table TableReference) *JoinBuilder {
    return &JoinBuilder{
        typ:   "INNER",  // 默认 INNER JOIN
        table: table,
    }
}

// LeftJoin 创建一个 LEFT JOIN
func LeftJoin(table TableReference) *JoinBuilder {
    return &JoinBuilder{
        typ:   "LEFT",
        table: table,
    }
}

// RightJoin 创建一个 RIGHT JOIN
func RightJoin(table TableReference) *JoinBuilder {
    return &JoinBuilder{
        typ:   "RIGHT",
        table: table,
    }
}

// On 是终结方法,使用 ON 条件
// 返回 Join,不能再调用 Using
// 这种设计可以避免用户同时调用 On 和 Using
func (j *JoinBuilder) On(cond Expression) *Join {
    return &Join{
        typ:   j.typ,
        table: j.table,
        cond:  cond,
    }
}

// Using 是终结方法,使用 USING 列名
// 返回 Join,不能再调用 On
func (j *JoinBuilder) Using(cols ...string) *Join {
    return &Join{
        typ:   j.typ,
        table: j.table,
        using: cols,
    }
}

JOIN 嵌套

JOIN 本身也可以进一步 JOIN,所以我们需要在 Join 上定义类似的方法:

// join.go 文件(续)

// Join 在当前 Join 的基础上继续 INNER JOIN
// 支持链式调用,形成嵌套 JOIN
func (j *Join) Join(table TableReference) *JoinBuilder {
    return &JoinBuilder{
        typ:   "INNER",
        // 注意:当前 Join 作为左操作数
        table: table,
    }
}

func (j *Join) LeftJoin(table TableReference) *JoinBuilder {
    return &JoinBuilder{
        typ:   "LEFT",
        table: table,
    }
}

func (j *Join) RightJoin(table TableReference) *JoinBuilder {
    return &JoinBuilder{
        typ:   "RIGHT",
        table: table,
    }
}
// 使用示例:嵌套 JOIN
// SELECT * FROM user u
// INNER JOIN order o ON u.id = o.user_id
// LEFT JOIN item i ON o.id = i.order_id

query := orm.NewSelector[User](db).
    From(orm.TableOf("user", "u")).
    Join(orm.TableOf("order", "o")).
        On(orm.C("u.Id").EQ(orm.C("o.UserId"))).
    LeftJoin(orm.TableOf("item", "i")).
        On(orm.C("o.Id").EQ(orm.C("i.OrderId")))

1.6 重构 Selector

引入 TableReference 后,我们需要重构 SelectorFrom 方法:

// selector.go 文件

// Selector 是 SELECT 语句的构造器
type Selector[T any] struct {
    core
    session Session

    // table FROM 子句
    // 从原来的 string 改为 TableReference
    // 这样可以支持 JOIN 和子查询
    table TableReference

    // selects 指定的列
    selects []Selectable

    // where WHERE 条件
    where   []Expression

    // ... 其它字段
}

// From 指定 FROM 子句
// 参数从 string 改为 TableReference
// 兼容旧用法:orm.TableOf("user") 传给 From
func (s *Selector[T]) From(table TableReference) *Selector[T] {
    s.table = table
    return s
}

构建 FROM 子句

// buildFrom 构建 FROM 子句
func (s *Selector[T]) buildFrom(sb *strings.Builder) error {
    // 使用类型断言判断是哪种 TableReference
    switch t := s.table.(type) {
    case nil:
        // 用户没有调用 From 方法
        // 使用模型默认的表名
        sb.WriteString(s.model.TableName)
    case *Table:
        // 普通表
        s.buildTable(sb, t)
    case *Join:
        // JOIN 查询
        if err := s.buildJoin(sb, t); err != nil {
            return err
        }
    }
    return nil
}

// buildTable 构建表引用
func (s *Selector[T]) buildTable(sb *strings.Builder, t *Table) {
    sb.WriteString(t.name)
    if t.alias != "" {
        sb.WriteString(" AS ")
        sb.WriteString(t.alias)
    }
}

// buildJoin 构建 JOIN 子句(支持嵌套)
func (s *Selector[T]) buildJoin(sb *strings.Builder, j *Join) error {
    // 1. 先构建左操作数(可能是 Table 或 Join)
    switch left := j.left.(type) {
    case *Table:
        s.buildTable(sb, left)
    case *Join:
        // 递归构建嵌套 JOIN
        if err := s.buildJoin(sb, left); err != nil {
            return err
        }
    }

    // 2. 构建 JOIN 类型和右表
    switch j.typ {
    case "INNER":
        sb.WriteString(" JOIN ")
    case "LEFT":
        sb.WriteString(" LEFT JOIN ")
    case "RIGHT":
        sb.WriteString(" RIGHT JOIN ")
    }

    // 3. 构建右表
    switch right := j.table.(type) {
    case *Table:
        s.buildTable(sb, right)
    case *Join:
        // 子 JOIN 需要括号包裹
        sb.WriteString("(")
        if err := s.buildJoin(sb, right); err != nil {
            return err
        }
        sb.WriteString(")")
    }

    // 4. 构建条件(ON 或 USING,互斥)
    if len(j.using) > 0 {
        sb.WriteString(" USING(")
        for i, col := range j.using {
            if i > 0 {
                sb.WriteString(", ")
            }
            sb.WriteString(col)
        }
        sb.WriteString(")")
    } else if j.cond != nil {
        sb.WriteString(" ON ")
        // 构建条件表达式
        if err := s.buildExpr(sb, j.cond); err != nil {
            return err
        }
    }

    return nil
}

1.7 列校验逻辑改造

在支持 JOIN 之前,列校验很简单——只需要看操作的元数据里面有没有这个列。支持 JOIN 之后,列可能有一个拥有者(owner)——例如 t1.col1,其中 t1 就是 col1 的拥有者。

⚠️ 新手必踩的坑:这里的列校验是"简化版"。文中 validateColumnowner != "" 时直接 return nil,并没有真去 TableReference 里反查该表到底有没有这个列。真实生产 ORM 要遍历所有 JOIN 涉及的表元数据逐个校验,否则拼错 orm.C("u.Name") 这类列名只能等到数据库报错才发现。

// selector.go 文件(续)

// buildColumn 构建列引用,支持表前缀
func (s *Selector[T]) buildColumn(sb *strings.Builder, c Column, withAlias bool) error {
    // 1. 如果用户指定了表前缀(owner)
    //    例如 orm.C("u.Id") 中的 "u"
    if c.owner != "" {
        // 在 JOIN 场景下,需要验证 owner 是否是某个表的别名
        // 这里简化处理,直接输出
        sb.WriteString(c.owner)
        sb.WriteString(".")
    }

    // 2. 查找列名
    if fd, ok := s.model.FieldMapByGoName[c.name]; ok {
        // 找到了字段,输出列名
        sb.WriteString(fd.ColName)
    } else {
        // 没找到,可能是用户写的原始列名
        sb.WriteString(c.name)
    }

    // 3. 输出别名(如果有)
    if withAlias && c.alias != "" {
        sb.WriteString(" AS ")
        sb.WriteString(c.alias)
    }
    return nil
}
// Column 结构体增加 owner 字段
type Column struct {
    name  string  // 字段名
    alias string  // 别名
    owner string  // 表前缀(owner),如 "u.Id" 中的 "u"
}

// C 支持 owner 的列构造
// 用法:
//   orm.C("Id")         → 输出 id
//   orm.C("u", "Id")    → 输出 u.id
func C(owner string, name ...string) Column {
    if len(name) > 0 {
        return Column{owner: owner, name: name[0]}
    }
    return Column{name: owner}
}

校验逻辑

支持 JOIN 后的列校验逻辑:

如果用户指定了表(owner):
  └── 检查指定的表上有没有这个列
      ├── 有 → 正常输出 owner.col
      └── 没有 → 报错:未知列

如果没有指定表:
  └── 走老逻辑(case nil 分支)
      └── 在当前模型的元数据中查找
// validateColumn 校验列是否有效
func (s *Selector[T]) validateColumn(c Column) error {
    if c.owner != "" {
        // 用户指定了表前缀
        // 需要检查该表是否有此列
        // 这里需要从 TableReference 中找到对应的表元数据
        // 简化处理:如果 JOIN 涉及多个表,需要查找所有表
        // 实际实现会更复杂
        return nil
    }
    // 没有指定表,走老逻辑
    _, ok := s.model.FieldMapByGoName[c.name]
    if !ok {
        return errs.NewErrUnknownField(c.name)
    }
    return nil
}

1.8 子查询

TableReference 的第三种实现是子查询。子查询也可以用来构造 JOIN 查询,或者直接用于 FROM 子句:

类比:子查询就像一个"临时视图"。你不想每次都 JOIN 一大张表,就先写一条 SELECT ... WHERE age > 18 当成一个临时小册子(子查询),给它起个别名 sub,外层再对 sub 继续筛选。对 ORM 来说,子查询和表是同一类东西(TableReference),只是它内部还抱着一个 Selector

下面这张图展示子查询作为 FROM 的用法:外层 SELECT 直接查一个"查询出来的结果集"。

flowchart LR
    O[外层 SELECT] -->|FROM| SQ[(子查询 sub)]
    SQ -->|内部持有| SEL[Selector
SELECT id,name FROM user WHERE age>18]
// subquery.go 文件

// Subquery 代表一个子查询
type Subquery struct {
    // selector 子查询的 Selector
    selector *Selector[any]
    // alias 子查询的别名
    alias    string
}

// tableReference 实现 TableReference 接口
func (s *Subquery) tableReference() {}

// SubqueryOf 创建一个子查询
// 用法:orm.SubqueryOf(selector, "sub")
func SubqueryOf(selector *Selector[any], alias string) *Subquery {
    return &Subquery{
        selector: selector,
        alias:    alias,
    }
}

// As 设置子查询别名
func (s *Subquery) As(alias string) *Subquery {
    return &Subquery{
        selector: s.selector,
        alias:    alias,
    }
}
// 使用示例:子查询作为 FROM

// SELECT * FROM (
//     SELECT id, name FROM user WHERE age > 18
// ) AS sub WHERE sub.id > 100

subQuery := orm.NewSelector[User](db).
    Select(orm.C("Id"), orm.C("Name")).
    From(orm.TableOf("user")).
    Where(orm.C("Age").GT(18))

result, err := orm.NewSelector[User](db).
    From(orm.SubqueryOf(subQuery, "sub")).
    Where(orm.C("sub.Id").GT(100)).
    GetMulti(ctx)

1.9 WHERE vs ON vs HAVING

子句作用位置说明
WHERE在分组前过滤对所有行生效,在 GROUP BY 之前执行
ONJOIN 条件控制 JOIN 时哪些行匹配,影响中间结果集大小
HAVING在分组后过滤对分组后的结果生效,在 GROUP BY 之后执行

面试要点:在 JOIN 查询里,一般建议尽量把条件放到 ON 上面,这样 JOIN 生成的中间数据要少很多,性能更好。

下面这张图把 WHERE / ON / HAVING 在"查询流水线"上的作用位置标出来,帮助理解为什么条件越早过滤越好:

flowchart LR
    J[JOIN 两表
按 ON 条件匹配] --> W[WHERE
分组前过滤行] W --> G[GROUP BY 分组] --> H[HAVING
分组后过滤]

1.10 JOIN 执行原理

JOIN 的执行原理可以近似理解为一个双重循环

 Nested Loop Join(嵌套循环连接):
 
 for each row r1 in left_table:        ← 遍历左表
     for each row r2 in right_table:   ← 遍历右表
         if r1.key == r2.key:          ← 检查匹配条件
             output(r1, r2)            ← 输出匹配的行
 
 优化:
 ├── Index Nested Loop Join:右表有索引,内层循环变成索引查找
 ├── Block Nested Loop Join:把左表分块,减少右表扫描次数
 └── Hash Join:用哈希表替代内层循环(MySQL 8.0+)

下面这张图把"嵌套循环 JOIN"的匹配过程画出来:外层遍历左表,内层遍历右表,匹配 key 才输出一行。

flowchart TD
    L[遍历左表每行 r1] --> R[遍历右表每行 r2]
    R -->|r1.key == r2.key| O[输出 r1,r2 组合行]
    R -->|不匹配| N[丢弃 或 填 NULL]

面试要点:JOIN 的执行原理算是热门面试题,可以近似理解为一个双重循环。不同 JOIN 类型的区别在于对不匹配行的处理:INNER JOIN 丢弃,LEFT JOIN 保留左表行(右表填 NULL),RIGHT JOIN 保留右表行(左表填 NULL)。


二、Protobuf 集成

类比:Protobuf 生成的 Go 代码就像工厂流水线批量印出来的表格,每张都长一样、只印了它该印的字段(protobuf / json tag)。我们想额外在表格上加一行"数据库列名",但流水线模具(插件)不支持——于是要么改模具(改插件源码),要么另想办法。本文走的是"改模具"这条侵入式路线。

下面这张图是定制 Protobuf 插件的工作流:写 .proto → 用改过的插件 protoc 生成带 orm tag 的代码 → ORM 直接复用。

flowchart LR
    P[user.proto] -->|protoc + 定制插件| G[user.pb.go
带 orm column tag] G -->|ORM 读取 tag| O[找到列名 user_name]

2.1 Protobuf 的问题

在元数据设计中,我们利用 Go 的 Tag(标签)来指定列名:

// 普通结构体:可以用 Tag 指定列名
type User struct {
    Name string `orm:"column=user_name"`
}

Protobuf 生成的代码没有自定义 Tag的能力:

// user.proto
message User {
    string name = 1;  // Protobuf 生成代码时不会加 orm tag
}

Protobuf 生成的 Go 代码类似这样:

// protoc 自动生成的代码(无法修改)
type User struct {
    // 只有 protobuf 的 tag,没有 orm 的 tag
    Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

我们希望能够达到这样的效果——让 Protobuf 生成的代码也能携带 ORM 的列名信息。

2.2 Protobuf 的局限性

Protobuf 虽然暴露了插件机制,但插件不能修改生成的 Go 代码——插件只能自己额外生成一些代码。

所以我们实际上不能直接利用 Protobuf 的插件机制来给字段添加 ORM Tag。

2.3 修改 Protobuf Go 插件

既然插件不能修改生成的代码,那我们只能走一条侵入式的路——修改 protobuf Go 插件的源码

步骤:
1. clone 原本的 protobuf Go 代码库(google/protobuf-go)
2. 修改 protobuf Go 的代码
   └── 找到生成 json 标签的位置,插入我们的 orm 标签
3. 安装修改后的 Go 插件
4. 执行 protoc 命令生成代码

为什么这种方法可行

我们知道 Protobuf 必然会生成 json 的标签:

// 原本生成的代码
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`

所以我们只需要找到生成 json 标签的位置,然后插入我们的代码:

// 修改插件后生成的代码
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty" orm:"column=user_name"`

修改示例

// 以下是 protobuf Go 插件源码中修改的核心逻辑(简化版)

// 原始代码(google/protobuf-go 内部)
func (g *generator) generateField(field *descriptor.FieldDescriptorProto) {
    // ... 原始逻辑 ...
    // 生成 json tag
    tag := fmt.Sprintf(`protobuf:"%s" json:"%s,omitempty"`,
        protobufTag, jsonName)
    g.P(field.GoName, " ", field.GoType, " `", tag, "`")
}

// 修改后的代码
func (g *generator) generateField(field *descriptor.FieldDescriptorProto) {
    // ... 原始逻辑 ...

    // 生成 json tag
    tag := fmt.Sprintf(`protobuf:"%s" json:"%s,omitempty"`,
        protobufTag, jsonName)

    // ===== 新增:添加 orm tag =====
    // 将字段名转为下划线命名作为列名
    ormColName := underscoreName(field.GoName)
    tag = tag + fmt.Sprintf(` orm:"column=%s"`, ormColName)
    // ==============================

    g.P(field.GoName, " ", field.GoType, " `", tag, "`")
}

注意:这是一种侵入式的修改方案,不过我们别无选择。你们公司可以维护一个自己定制过的 Protobuf Go 插件。

关于 Field Option

Protobuf 有一个 field option 扩展机制,理论上可以用来添加额外的属性:

// 自定义 field option
import "orm/options.proto";

message User {
    // 希望通过 option 指定列名
    string name = 1 [(orm.column) = "user_name"];
}

但实际测试后发现,field option 的值在生成 Go 代码时无法直接写入到 Tag 中,所以最终还是只能用修改插件源码的方案。

⚠️ 新手必踩的坑:别迷信 field option 能省事。很多人第一反应是用 [(orm.column) = "..."] 这种扩展字段来传列名,但 Protobuf 的设计决定这些 option 值只能在运行时通过反射 API 读取,无法在代码生成阶段写进 Go 的 struct tag。实测走不通,直接上"改插件源码"更靠谱。

2.4 使用定制 Protobuf 的工作流

开发流程:

1. 编写 .proto 文件
   ┌─────────────────┐
   │  user.proto      │
   │  message User {  │
   │    string name=1;│
   │  }               │
   └────────┬────────┘
            │
2. 执行 protoc 命令(使用定制插件)
            │
            ▼
   ┌──────────────────────────────────────┐
   │  user.pb.go(自动生成)               │
   │  type User struct {                  │
   │    Name string `protobuf:"..."       │
   │    json:"name,omitempty"             │
   │    orm:"column=name"`  ← 定制插件添加 │
   │  }                                   │
   └──────────────────────────────────────┘
            │
3. 在 ORM 中直接使用 Protobuf 生成的结构体
            │
            ▼
   ┌──────────────────────────────────────┐
   │  db, _ := orm.Open("mysql", dsn)     │
   │  user, _ := orm.NewSelector[User](db)│
   │    .Where(orm.C("Name").EQ("大明"))   │
   │    .Get(ctx)                          │
   │  // ORM 通过 orm tag 找到列名 "name"  │
   └──────────────────────────────────────┘

三、代码生成辅助方法

3.1 为什么需要代码生成

我们的 Predicate 设计有一些缺陷,这些缺陷是可以通过代码生成来改进的:

// 当前的写法:用户需要手写字段名字符串
// 容易写错,没有编译期检查
users, err := orm.NewSelector[User](db).
    Where(orm.C("FirstName").EQ("大明")).  // "FirstName" 是字符串,写错了编译器不会报错
    Get(ctx)

// 期望的写法:通过代码生成,提供类型安全的常量和方法
users, err := orm.NewSelector[User](db).
    Where(UserColumns.FirstName.EQ("大明")).  // 编译器能检查 FirstName 是否存在
    Get(ctx)

我们需要生成两类东西:

  1. 字段名的常量:避免手写字符串
  2. Predicate 辅助方法:简化查询条件的构造

3.2 生成目标代码

// ================================
// 原始模型文件:model/user.go
// ================================
package model

type User struct {
    Id        int64
    FirstName string
    LastName  string
    Age       int
}

// ================================
// 代码生成的文件:model/user_gen.go
// ================================
// 注意:这个文件是自动生成的,不要手动修改

package model

import "orm"

// UserColumns 包含 User 的所有字段常量
// 使用常量避免手写字符串,提供编译期检查
var UserColumns = struct {
    Id        orm.Column
    FirstName orm.Column
    LastName  orm.Column
    Age       orm.Column
}{
    Id:        orm.Column{name: "Id"},
    FirstName: orm.Column{name: "FirstName"},
    LastName:  orm.Column{name: "LastName"},
    Age:       orm.Column{name: "Age"},
}

// UserPredicates 提供 User 的 Predicate 快捷构造方法
var UserPredicates = struct {
    Id        func(val any) orm.Predicate
    FirstName func(val any) orm.Predicate
    LastName  func(val any) orm.Predicate
    Age       func(val any) orm.Predicate
}{
    Id:        func(val any) orm.Predicate { return orm.C("Id").EQ(val) },
    FirstName: func(val any) orm.Predicate { return orm.C("FirstName").EQ(val) },
    LastName:  func(val any) orm.Predicate { return orm.C("LastName").EQ(val) },
    Age:       func(val any) orm.Predicate { return orm.C("Age").EQ(val) },
}
// 使用生成的辅助方法
// 比手写字符串更安全、更方便

// 使用列常量
users, err := orm.NewSelector[User](db).
    Where(UserColumns.FirstName.EQ("大明")).
    Get(ctx)

// 使用 Predicate 辅助方法
users, err = orm.NewSelector[User](db).
    Where(UserPredicates.Age(28)).
    Get(ctx)

// 组合使用
users, err = orm.NewSelector[User](db).
    Where(UserPredicates.FirstName("大明").
        And(UserPredicates.Age(28))).
    Get(ctx)

3.3 什么是 AST

AST(Abstract Syntax Tree,抽象语法树)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

源代码:                      AST 树:

type User struct {           StructType
    Name string                  ├── Fields
    Age  int                     │   ├── Field: Name string
}                               │   └── Field: Age int
                                └── Name: "User"

重要:并发、反射和 AST 三者是构建复杂系统(中间件)的基石,是真正的高级工具。

3.4 AST 编程的应用场景

用途说明典型项目
篡改源码修改 AST 来改变源码行为-
代码生成Go 没有动态代理,代码生成大行其道GORM Gen、ENT
依赖注入编译时生成依赖注入代码Google Wire
语法检查静态分析工具golangci-lint
格式化代码格式化工具gofmt
自动补全IDE 智能提示gopls

3.5 AST 编程基本步骤

// ast_gen.go 文件

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    // ========== 步骤 1:创建 FileSet ==========
    // FileSet 用于记录源代码的位置信息
    // 每个 AST 节点都关联一个位置,FileSet 管理这些位置
    fset := token.NewFileSet()

    // ========== 步骤 2:解析源文件 ==========
    // parser.ParseFile 将 Go 源文件解析为 AST
    // 参数:
    //   fset: 位置信息管理器
    //   filename: 文件路径(可以为空)
    //   src: 源代码内容
    //   mode: 解析模式(控制解析的详细程度)
    f, err := parser.ParseFile(fset, "user.go", nil, parser.ParseComments)
    if err != nil {
        panic(err)
    }

    // ========== 步骤 3:实现 ast.Visitor 接口 ==========
    // Visitor 用于遍历 AST 树
    visitor := &structVisitor{}

    // ========== 步骤 4:遍历 AST ==========
    // ast.Walk 从根节点开始遍历整棵 AST 树
    ast.Walk(visitor, f)
}

// structVisitor 实现了 ast.Visitor 接口
// 用于遍历 AST 并查找结构体定义
type structVisitor struct {
    // 记录找到的结构体信息
    structs []structInfo
}

// structInfo 存储解析到的结构体信息
type structInfo struct {
    Name   string       // 结构体名
    Fields []fieldInfo  // 字段列表
}

// fieldInfo 存储字段信息
type fieldInfo struct {
    Name string  // 字段名
    Type string  // 字段类型
}

// Visit 实现 ast.Visitor 接口
// 每遍历到一个 AST 节点都会调用此方法
// 返回值:如果返回非 nil,则继续遍历子节点;返回 nil 则跳过子节点
func (v *structVisitor) Visit(node ast.Node) ast.Visitor {
    // node 可能为 nil(遍历结束时)
    if node == nil {
        return nil
    }

    // 使用类型断言判断节点类型
    switch n := node.(type) {
    case *ast.TypeSpec:
        // TypeSpec 代表类型声明(type xxx ...)
        // 检查是否是结构体
        if structType, ok := n.Type.(*ast.StructType); ok {
            info := structInfo{
                Name: n.Name.Name,  // 结构体名
            }
            // 遍历结构体的字段
            for _, field := range structType.Fields.List {
                // 一个 field 可能对应多个名字(如 Name, Email string)
                for _, name := range field.Names {
                    info.Fields = append(info.Fields, fieldInfo{
                        Name: name.Name,
                        // 获取类型名(简化处理)
                        Type: fmt.Sprintf("%v", field.Type),
                    })
                }
            }
            v.structs = append(v.structs, info)
        }
    }

    // 返回自身,继续遍历子节点
    return v
}

3.6 Visitor 设计模式详解

Visitor(访问者)设计模式常用于遍历复杂的数据结构,典型的就是树形结构。

Visitor 模式的核心思想:

对于 Node(节点):
  └── 接收 Visitor,在实现中调用 Visitor.Visit(node)

对于 Visitor(访问者):
  └── 接收 Node,做类型判断,判断能不能处理该 Node

遍历过程:
  ast.Walk(visitor, rootNode)
       │
       ▼
  rootNode.Accept(visitor)
       │
       ├── visitor.Visit(rootNode)  → 返回 childVisitor
       │                                 │
       └── for each child:               ▼
              ast.Walk(childVisitor, child)
                   │
                   └── child.Accept(childVisitor)
                           │
                           └── ...继续递归...

Go AST 的 Visitor 特殊之处

// ast.Visitor 接口定义
type Visitor interface {
    // Visit 方法接收一个 Node
    // 返回值也是一个 Visitor——用于进一步遍历子节点
    // 如果返回 nil,则不再遍历子节点
    Visit(node Node) (w Visitor)
}
// ast.Walk 的简化实现
func Walk(v Visitor, node Node) {
    if node == nil {
        return
    }

    // 调用 Visitor 的 Visit 方法
    // 如果返回的 Visitor 不为 nil,则继续遍历子节点
    if v = v.Visit(node); v == nil {
        return  // 返回 nil,停止遍历子节点
    }

    // 根据节点类型,遍历所有子节点
    switch n := node.(type) {
    case *ast.File:
        // 遍历文件中的所有声明
        for _, decl := range n.Decls {
            Walk(v, decl)
        }
    case *ast.GenDecl:
        // 遍历通用声明(type、var、const、import)
        for _, spec := range n.Specs {
            Walk(v, spec)
        }
    case *ast.TypeSpec:
        // 遍历类型声明的类型部分
        Walk(v, n.Type)
    case *ast.StructType:
        // 遍历结构体的字段
        for _, field := range n.Fields.List {
            Walk(v, field)
        }
    // ... 还有几十种节点类型
    }

    // 遍历完毕,调用 Visit(nil) 通知 Visitor
    v.Visit(nil)
}

3.7 AST 节点类型

ast.Node 接口代表了 AST 的节点。任何一个 Go 语句在 AST 里面都代表一个节点。有三个关键子接口:

ast.Node(根接口)
├── ast.Expr(表达式)
│   ├── 基本类型:int, string, bool
│   ├── 方法调用:foo.Bar()
│   ├── 数组索引:arr[0]
│   ├── 二元运算:a + b
│   └── ...(约 30 种)
├── ast.Stmt(语句)
│   ├── 赋值语句:a = b
│   ├── if 语句:if x { ... }
│   ├── for 语句:for { ... }
│   └── ...(约 20 种)
└── ast.Decl(声明)
    ├── GenDecl:通用声明(type、var、const、import)
    └── FuncDecl:方法/函数声明

提示:不需要死记这些类型。写代码的时候再去看具体的实现,因为总共有 69 种实现。

GenDecl —— 通用声明

// GenDecl 代表通用声明
// 通过 Tok 字段判断具体是什么声明:
//   token.TYPE    → 类型声明 (type)
//   token.VAR     → 变量声明 (var)
//   token.CONST   → 常量声明 (const)
//   token.IMPORT  → 导入声明 (import)

type GenDecl struct {
    Doc  *ast.CommentGroup  // 文档注释
    TokPos token.Pos         // 关键字位置
    Tok   token.Token        // 关键字:type、var、const、import
    Lparen token.Pos         // 左括号位置(如果有的话)
    Specs []ast.Spec         // 具体声明内容
    Rparen token.Pos         // 右括号位置
}

FuncDecl —— 方法声明

// FuncDecl 代表方法/函数声明
type FuncDecl struct {
    Doc  *ast.CommentGroup   // 文档注释
    Recv *ast.FieldList      // 接收器(只有方法有,普通函数为 nil)
    Name *ast.Ident          // 方法名
    Type *ast.FuncType       // 方法签名(包含泛型参数、参数、返回值)
    Body *ast.BlockStmt      // 方法体
}

StructType —— 结构体声明

// StructType 代表一个结构体声明
type StructType struct {
    Struct token.Pos    // struct 关键字位置
    Fields *ast.FieldList  // 字段列表
    // 注意:Fields 里面找不到定义在该结构体上的方法
    // 方法声明在 ast.File 的 Decls 里(作为 FuncDecl)
}

// ast.FieldList 是字段列表
type FieldList struct {
    Opening token.Pos  // 左括号位置
    List    []*Field   // 字段列表
    Closing token.Pos  // 右括号位置
}

// ast.Field 代表一个字段
type Field struct {
    Doc     *ast.CommentGroup  // 文档注释
    Names   []*Ident           // 字段名(可能多个,如 Name, Email string)
    Type    Expr               // 字段类型
    Tag     *BasicLit          // Tag(标签)
    Comment *ast.CommentGroup  // 行注释
}

3.8 代码生成实现

现在我们把 AST 编程和模板结合起来,实现代码生成:

// gen/main.go 文件

package main

import (
    "bytes"
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "os"
    "strings"
    "text/template"
)

// ========== 数据结构 ==========

// StructInfo 存储解析到的结构体信息
type StructInfo struct {
    Package string      // 包名
    Name    string      // 结构体名
    Fields  []FieldInfo // 字段列表
}

// FieldInfo 存储字段信息
type FieldInfo struct {
    Name string  // 字段名(Go 字段名)
    Type string  // 字段类型
}

// ========== AST 解析器 ==========

// Parser 解析 Go 源文件,提取结构体信息
type Parser struct {
    structs []StructInfo
    pkgName string
}

// ParseFile 解析指定的 Go 源文件
func (p *Parser) ParseFile(filename string) error {
    // 1. 创建 FileSet
    fset := token.NewFileSet()

    // 2. 解析源文件
    f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
    if err != nil {
        return err
    }

    // 3. 记录包名
    p.pkgName = f.Name.Name

    // 4. 遍历 AST
    ast.Inspect(f, func(node ast.Node) bool {
        // ast.Inspect 是 ast.Walk 的简化版
        // 返回 true 继续遍历子节点,返回 false 跳过

        // 查找类型声明
        typeSpec, ok := node.(*ast.TypeSpec)
        if !ok {
            return true
        }

        // 检查是否是结构体
        structType, ok := typeSpec.Type.(*ast.StructType)
        if !ok {
            return true
        }

        // 提取结构体信息
        info := StructInfo{
            Package: p.pkgName,
            Name:    typeSpec.Name.Name,
        }

        // 遍历字段
        for _, field := range structType.Fields.List {
            // 跳过非导出字段
            for _, name := range field.Names {
                if !name.IsExported() {
                    continue
                }
                info.Fields = append(info.Fields, FieldInfo{
                    Name: name.Name,
                    Type: exprToString(field.Type),
                })
            }
        }

        p.structs = append(p.structs, info)
        return true
    })

    return nil
}

// exprToString 将 AST 表达式转为类型字符串
// 例如:*ast.Ident → "string",*ast.SelectorExpr → "time.Time"
func exprToString(expr ast.Expr) string {
    switch t := expr.(type) {
    case *ast.Ident:
        // 简单类型,如 string、int
        return t.Name
    case *ast.SelectorExpr:
        // 包名.类型,如 time.Time
        return exprToString(t.X) + "." + t.Sel.Name
    case *ast.StarExpr:
        // 指针类型,如 *string
        return "*" + exprToString(t.X)
    default:
        return fmt.Sprintf("%v", expr)
    }
}

// ========== 代码生成 ==========

// 生成模板
const genTemplate = `// Code generated by ORM gen. DO NOT EDIT.

package {{.Package}}

import "orm"

{{range .Structs}}
// {{.Name}}Columns 包含 {{.Name}} 的所有字段常量
var {{.Name}}Columns = struct {
{{range .Fields}}    {{.Name}} orm.Column
{{end}}}{
{{range .Fields}}    {{.Name}}: orm.Column{name: "{{.Name}}"},
{{end}}}

// {{.Name}}Predicates 提供 {{.Name}} 的 Predicate 快捷构造方法
var {{.Name}}Predicates = struct {
{{range .Fields}}    {{.Name}} func(val any) orm.Predicate
{{end}}}{
{{range .Fields}}    {{.Name}}: func(val any) orm.Predicate { return orm.C("{{.Name}}").EQ(val) },
{{end}}}
{{end}}
`

// GenConfig 代码生成配置
type GenConfig struct {
    InputFile  string  // 输入文件路径(模型文件)
    OutputFile string  // 输出文件路径(生成文件)
}

// Generate 执行代码生成
func Generate(config GenConfig) error {
    // 1. 解析源文件
    p := &Parser{}
    if err := p.ParseFile(config.InputFile); err != nil {
        return err
    }

    // 2. 准备模板数据
    data := struct {
        Package string
        Structs []StructInfo
    }{
        Package: p.pkgName,
        Structs: p.structs,
    }

    // 3. 解析模板
    tmpl, err := template.New("gen").Parse(genTemplate)
    if err != nil {
        return err
    }

    // 4. 执行模板渲染
    var buf bytes.Buffer
    if err := tmpl.Execute(&buf, data); err != nil {
        return err
    }

    // 5. 写入输出文件
    return os.WriteFile(config.OutputFile, buf.Bytes(), 0644)
}

func main() {
    // 执行代码生成
    err := Generate(GenConfig{
        InputFile:  "model/user.go",
        OutputFile: "model/user_gen.go",
    })
    if err != nil {
        fmt.Printf("代码生成失败: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("代码生成成功!")
}

3.9 代码生成流程图

下面这张图是代码生成的完整链路,和下面的 ASCII 图对照看更清楚:

flowchart TD
    S[model/user.go 源文件] -->|parser.ParseFile| A[AST 树]
    A -->|ast.Inspect 遍历| I[StructInfo 字段信息]
    I -->|template.Execute 渲染| G[user_gen.go
UserColumns + UserPredicates]
代码生成完整流程:

┌──────────────────────────┐
│  model/user.go(源文件)   │
│  type User struct {      │
│    Id   int64            │
│    Name string           │
│    Age  int              │
│  }                       │
└──────────┬───────────────┘
           │
           ▼  parser.ParseFile()
┌──────────────────────────┐
│  AST 树                   │
│  └── TypeSpec            │
│      ├── Name: "User"    │
│      └── StructType      │
│          └── Fields      │
│              ├── Id:int64│
│              ├── Name:str│
│              └── Age:int │
└──────────┬───────────────┘
           │
           ▼  ast.Inspect() / Visitor
┌──────────────────────────┐
│  StructInfo 结构体信息     │
│  {                       │
│    Package: "model"      │
│    Name: "User"          │
│    Fields: [             │
│      {Id, int64},        │
│      {Name, string},     │
│      {Age, int}          │
│    ]                     │
│  }                       │
└──────────┬───────────────┘
           │
           ▼  template.Execute()
┌──────────────────────────┐
│  model/user_gen.go(生成)│
│  var UserColumns = ...   │
│  var UserPredicates = ...│
└──────────────────────────┘

3.10 代码生成的常用场景

场景说明典型项目
数据库查询生成根据模型生成类型安全的查询代码GORM Gen、ENT
增删改查代码生成标准 CRUD 代码Beego Code Generator
依赖注入编译时生成依赖注入代码Google Wire
前端代码从后端模型生成前端 TypeScript 接口openapi-generator
Protobuf从 .proto 生成各语言代码protoc
Mock 代码生成接口的 Mock 实现mockery

面试要点:代码生成和动态代理的区别?绝大多数语言的动态代理机制就是语言的运行时帮你做了代码生成(例如 Java),而 Go 因为运行时和反射无法做到动态生成新的结构体,所以只能采用代码生成方案。


四、面试要点总结

JOIN 与复杂查询

  • GORM 的 Preload 是什么? 本质上就是一个 JOIN 查询,并且严格来说,在 Go 语言里面是很难实现 lazy load 的。GORM 的 Preload 就是通过 Join 把相关的数据都查询出来,并且组装成结构体。

  • WHERE、ON 和 HAVING 的区别? 主要考察的是 WHERE 和 ON。在 JOIN 查询里面,一般的建议都是尽量把条件放到 ON 上面,这样 JOIN 生成的中间数据要少很多。

  • JOIN、LEFT JOIN 和 RIGHT JOIN 的区别? INNER JOIN 只返回匹配的行;LEFT JOIN 返回左表所有行,右表不匹配的为 NULL;RIGHT JOIN 返回右表所有行,左表不匹配的为 NULL。

  • JOIN 的执行原理? 可以近似理解为一个双重循环。优化方案包括 Index Nested Loop Join(索引嵌套循环)、Block Nested Loop Join(块嵌套循环)和 Hash Join(哈希连接)。

Protobuf

  • 怎么修改 Protobuf 生成的代码? 生成好的代码从实践上来说是不应该修改的,而很不幸的是,Protobuf 对应不同插件生成的目标代码,只能改插件的源码。

  • 怎么为 Protobuf 的字段添加额外的属性? 可以通过 field option 来增加额外的属性,但是这种新的属性需要你自己写代码解析。最终方案是修改 Protobuf Go 插件源码。

AST 与代码生成

  • 什么是 AST? 是源代码的一种树形结构的表示,我们可以通过修改 AST 来修改源码。

  • AST 编程有什么用? 语法检查、格式检查、错误检查、自动补全、优化代码、代码生成。基本上就是只要你够强,用 AST 你就可以为所欲为,甚至设计新的语言。

  • Go 有没有注解? 没有,但是我们可以用 AST 来自己设计。

  • Visitor 设计模式? Visitor 模式主要用于遍历复杂结构体,要点在于接收一个节点作为输入,并且返回一个遍历子节点的 Visitor。

  • 代码生成的常用场景? 样板代码都可以考虑使用代码生成来替换掉,比如生成数据库查询(ENT)、增删改查代码、前端代码等。

  • 代码生成和动态代理的区别? 绝大多数语言的动态代理机制就是语言的运行时帮你做了代码生成(如 Java),而 Go 因为运行时和反射无法做到动态生成新的结构体,所以只能采用代码生成方案。

ORM 框架完整能力图

到这里,ORM 框架系列教程全部完成。回顾整个系列,ORM 框架的完整能力包括:

ORM 框架完整能力图
│
├── SQL 构造(Builder 模式)
│   ├── SELECT(Selector)
│   │   ├── 基础查询(Where、From、Get、GetMulti)
│   │   ├── 指定列与聚合函数(Selectable、Column、Aggregate)
│   │   ├── JOIN 查询(TableReference、Join、JoinBuilder)
│   │   └── 子查询(Subquery)
│   ├── INSERT(Inserter)
│   │   ├── 单条/批量插入
│   │   ├── 指定列插入
│   │   └── UPSERT(OnDuplicate、Assignment)
│   ├── UPDATE(Updater)
│   └── DELETE(Deleter)
│
├── 元数据(反射 + 缓存)
│   ├── Model / Field(表名、列名、类型、偏移量)
│   ├── Registry 注册中心(并发安全缓存)
│   ├── 自定义表名/列名(Tag、接口、Option)
│   └── Protobuf 集成(修改插件源码)
│
├── 结果集处理(valuer 抽象)
│   ├── 反射方案(reflectValue)
│   ├── unsafe 方案(unsafeValue)
│   └── Creator 工厂模式
│
├── 事务管理
│   ├── Tx 结构体 + Session 抽象
│   ├── 事务闭包(DoTx)
│   ├── RollbackIfNotCommit
│   └── 事务扩散(context 传递)
│
├── AOP 方案(Middleware 洋葱模型)
│   ├── QueryContext / QueryResult / Handler / Middleware
│   ├── 日志 / 追踪 / 监控中间件
│   └── Dry Run 支持
│
├── 方言抽象(Dialect)
│   ├── standardSQL / mysqlDialect / sqlite3Dialect
│   └── builder 公共抽象
│
├── 测试体系
│   ├── 单元测试(sqlmock)
│   ├── 集成测试(TestSuite + Docker + go build 标签)
│   └── 原生查询(RawQuerier 兜底)
│
└── 代码生成
    ├── AST 解析(Visitor 模式)
    ├── 字段常量生成(UserColumns)
    ├── Predicate 辅助方法生成(UserPredicates)
    └── Protobuf 集成(定制 protoc 插件)

ORM 框架的核心可以总结为三点:构造 SQL设计元数据处理结果集。这三点是 ORM 的基石。在此之上,事务管理保证了数据一致性,AOP 方案提供了横切关注点的解决方案,方言抽象实现了多数据库兼容,测试体系确保了代码质量,代码生成提升了开发效率。这就是一个完整 ORM 框架的全貌。


自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. INNER JOIN / LEFT JOIN / RIGHT JOIN 的语义区别是什么?各自对"不匹配的行"怎么处理?
  2. 为什么 Go 里很难实现 lazy load?GORM 的 Preload 本质是什么?
  3. WHERE、ON、HAVING 分别什么时候执行?为什么建议把 JOIN 条件放到 ON 上?
  4. JOIN 的执行原理近似什么?Index Nested Loop Join 和 Hash Join 各自解决了什么问题?
  5. 为什么 Protobuf 字段的额外属性不能靠 field option 直接写进 Go tag?AST 代码生成相比 Java 动态代理的本质区别是什么?

动手练习(建议真做一遍)

  1. orm.TableOf / Join / LeftJoin 拼一个嵌套 JOIN(user JOIN order LEFT JOIN item),打印生成的 SQL 验证括号与 ON 条件。
  2. 把文中 ast_gen.go 跑一遍,扫描你自己项目里的 model 包,生成 UserColumns / UserPredicates,并在查询里替换手写字符串。
  3. 写一条聚合查询,把它改写成"子查询作为 FROM"的写法,对比两者生成的 SQL、可读性与执行计划。

本章小结

  • 复杂查询:用 TableReference(Table / Join / Subquery 三实现)统一抽象 FROM 子句,JOIN 可任意嵌套;执行上近似双重循环,条件越早(放 ON)过滤,中间结果越小。
  • Protobuf 集成:生成代码无法自带 orm tag,field option 也救不了,唯一现实方案是侵入式修改 protoc Go 插件源码,在生成 json tag 处顺手拼上 orm:"column=..."
  • 代码生成:基于 AST(parser.ParseFileast.Inspect 遍历 → text/template 渲染)把模型字段变成类型安全的 UserColumns / UserPredicates,本质是"编译期替你写样板代码",这是 Go 没有运行时动态代理下的必然选择。
  • 至此 ORM 系列全部完结,下一阶段可以把这些能力接入真实数据库与压测,验证在大数据量下的表现。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!