夜雨聆风学习资料网

ARTICLE · 1002842

Gorm链式调用API源码解析 - Distinct

Gorm链式调用API源码解析 - Distinct

Distinct 方法解析

一、方法签名与原型

// Distinct specify distinct fields that you want querying////  // Select distinct names of users//  db.Distinct("name").Find(&results)//  // Select distinct name/age pairs from users//  db.Distinct("name", "age").Find(&results)func(db *DB) Distinct(args ...interface{}) (tx *DB)

参数说明:

  • • db *DB:GORM DB 指针接收者
  • • args ...interface{}:可选的可变参数,表示需要 DISTINCT 的字段名列表。如果传入参数,会被转发给 Select 方法同时设置 SELECT 字段;如果不传参数,则仅将当前已有的 Select 字段标记为 DISTINCT

返回值:

  • • tx *DB:新的 DB 实例副本,用于链式调用

二、完整源码

func(db *DB) Distinct(args ...interface{}) (tx *DB) {    tx = db.getInstance()    tx.Statement.Distinct = trueiflen(args) > 0 {        tx = tx.Select(args[0], args[1:]...)    }return}

三、逐行原理讲解

第 1 行:方法声明

func(db *DB) Distinct(args ...interface{}) (tx *DB) {
  • • 与多数链式 API 类似,返回 *DB 用于链式调用
  • • 参数 args ...interface{} 为可变参数,使用 interface{} 而非 string 是为了兼容 Select 方法多样化的入参形式(字符串、切片、map 等)
  • • 即使不传任何参数(db.Distinct())也是合法调用,语义为:对后续 SELECT 的字段启用去重

第 2 行:获取副本实例

    tx = db.getInstance()

标准的副本克隆操作。无论 Distinct 后续逻辑如何,都保证不污染原始 DB 对象。

第 3 行:设置 Distinct 标志位

    tx.Statement.Distinct = true

这是 Distinct 方法的核心作用。它将 Statement 上的布尔字段 Distinct 设为 true。

这个标志位在后续 SQL 编译阶段会被读取:

  • • 在 clause.Select 结构体的 Build 方法中,如果 Distinct == true,会在字段列表前插入关键字 DISTINCT
  • • 即编译时会生成:SELECT DISTINCT field1, field2 FROM ... 而非默认的 SELECT field1, field2 FROM ...

值得注意的是,Distinct 本身不直接操纵 Clauses map,而是通过 Statement 级别的字段影响 SELECT clause 的构建。这是一种"标志位驱动 Clause"的间接模式。

第 4-6 行:如果传入了字段参数,则转发调用 Select

iflen(args) > 0 {        tx = tx.Select(args[0], args[1:]...)    }

这是一个非常精妙的便捷性设计:

  • • 如果用户没有在 Distinct 中传字段,则需要显式搭配 .Select(...) 使用
  • • 如果用户传了字段,则相当于自动为其调用了 Select,省去了一次方法链调用
  • • 注意转发的参数形式是 args[0] 单独传,args[1:] 作为后续可变参数展开。这是因为 Select 方法的签名是 Select(query interface{}, args ...interface{}):第一个参数是主参数(可以是 string、[]string 等),后续是可选参数

由于 Select 本身会返回一个新的 tx 副本,所以这里要将返回值重新赋值给 tx,确保链式调用的状态能正确叠加。

第 7 行:返回 tx

return

裸返回 tx,返回的副本既包含了 Distinct = true 的标志,又(如果有参数的话)包含了新设置的 SELECT 字段。


四、核心设计思路

4.1 组合优于扩展:复用 Select 逻辑

Distinct 方法只有 7 行代码,其中的关键设计决策是:不重复实现字段解析逻辑,而是复用已有的 Select 方法。

如果 Distinct 自己实现一套字段解析逻辑(区分 []string、string、带占位符 SQL 等),不仅会和 Select 形成大量代码重复,还会导致未来 Select 逻辑更新时 Distinct 必须同步维护的一致性问题。

通过转发给 Select 方法:

  • • 代码量最小化(DRY 原则)
  • • 行为一致性有保障:Distinct("name", "age") 和 Select("name", "age").Distinct() 结果完全一致
  • • 未来 Select 新增能力时(如命名参数 @ 形式),Distinct 自动获得相同能力

4.2 标志位模式的声明式设计

SQL 层面的 SELECT DISTINCT 在语法上被视为对 SELECT 子句的修饰,而非独立子句。GORM 在建模时也忠实反映了这一点:

  • • 没有单独的 Clause.Distinct 子句
  • • 而是在 Statement 上设置一个布尔标志位 Distinct
  • • 最终由 clause.Select.Build() 方法在编译时读取该标志并生成 DISTINCT 关键字

这种"标志位 + 关联 Clause 编译时消费"的模式,比设计独立子句更贴近 SQL 真实语法结构,也减少了 Clause 合并时的复杂度。

4.3 两种使用模式的统一

Distinct 的设计允许用户在两种心智模型中自由切换:

模式 A:把 DISTINCT 当作修饰符

db.Select("name", "age").Distinct().Find(&results)

模式 B:把 DISTINCT 当作 Select 的变体

db.Distinct("name", "age").Find(&results)

两种模式在 Statement 状态上完全等价。用户可以按自己的习惯选择写法,API 同时支持两者而不产生额外成本。

4.4 与 Select 的前向兼容

第 4 行的 if len(args) > 0 判断是一个前向兼容设计。如果用户调用 db.Distinct() 不带参数,意味着他们想先设置标志位,稍后再通过 Select 指定字段。这在动态查询构造中是常见需求:

query := db.Model(&User{}).Distinct()if needOnlyName {    query = query.Select("name")} else {    query = query.Select("*")}query.Find(&results)

4.5 参数转发的边界处理

注意转发形式是 tx.Select(args[0], args[1:]...) 而非 tx.Select(args...)。这对应 Select 方法签名的不对称性:第一个参数叫 query(主类型),后续参数叫 args(绑定变量或额外字段)。这种转发确保了:

  • • Distinct("name", "age") → Select("name", "age") — 两个字段字符串,正确
  • • Distinct([]string{"name", "age"}) → Select([]string{"name", "age"}) — 切片形式,正确
  • • Distinct("name = ? OR name = ?", "a", "b") → 即使传入占位符形式,Select 也能正确处理

五、使用示例

示例 1:单列去重查询

// Go 代码var names []stringdb.Model(&User{}).Distinct("name").Find(&names)

生成的 SQL:

SELECTDISTINCT name FROM users

说明:

  • • Distinct 同时完成了"设置标志位"和"调用 Select('name')"两件事
  • • 结果:返回所有不重复的用户姓名

示例 2:多列组合去重

// Go 代码type NameAge struct {    Name string    Age  int}var results []NameAgedb.Table("users").Distinct("name", "age").Scan(&results)

生成的 SQL:

SELECTDISTINCT name, age FROM users

说明:

  • • 传入 "name" 和 "age" 两个参数,转发为 Select("name", "age")
  • • 多列去重时 SQL 会按 (name, age) 组合进行去重
  • • 使用 Scan 接收自定义结构体结果

示例 3:与 Where 条件、Order 组合使用(先标记 DISTINCT 后 Select)

// Go 代码var users []Userdb.Model(&User{}).    Distinct().    Where("age > ?", 18).    Select("city").    Order("city").    Find(&users)

生成的 SQL:

SELECTDISTINCT city FROM users WHERE age >18ORDERBY city

说明:

  • • 这里使用了"先标记 DISTINCT + 后 Select"的拆分模式
  • • 适合中间需要动态插入条件的场景
  • • WHERE 和 ORDER BY 不会影响 DISTINCT 修饰作用,最终 SQL 正确生成 DISTINCT

与Ai配合,对gorm的链式api中的Distinct方法的源码做了一个解读,记录到公众号,方便后续查阅。

相关学习资料