Revert "Revert "feat(database/gdb): Added OmitZero function to filter zero value parameters"" - #4819
Revert "Revert "feat(database/gdb): Added OmitZero function to filter zero value parameters""#4819hailaz wants to merge 3 commits into
Conversation
… zero va…" This reverts commit 9de28eb.
There was a problem hiding this comment.
Pull request overview
This PR re-applies the previously reverted OmitZero capability to GoFrame’s gdb ORM by introducing a new empty.IsZero helper and wiring a new model option through WHERE/HAVING formatting and DATA filtering.
Changes:
- Add
internal/empty.IsZerofor “zero value” detection (distinct fromIsEmpty, notably keeping non-nil empty slices/maps as non-zero). - Add
Model.OmitZero/OmitZeroWhere/OmitZeroDataoptions and propagate them into condition formatting and insert/update data filtering. - Add unit coverage for
IsZeroandOmitZero*behaviors (including slice/map edge cases and builder inheritance).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/empty/empty.go | Introduces IsZero zero-value detection with pointer tracing support. |
| internal/empty/empty_z_unit_test.go | Adds unit tests validating IsZero semantics vs IsEmpty. |
| database/gdb/gdb_model_utility.go | Filters insert/update DATA maps using the new OmitZeroData option. |
| database/gdb/gdb_model_select.go | Enables OmitZeroWhere behavior for HAVING condition formatting. |
| database/gdb/gdb_model_option.go | Adds new option bits and exposes OmitZero* model methods. |
| database/gdb/gdb_model_builder.go | Ensures WhereBuilder inherits/applies OmitZeroWhere during build. |
| database/gdb/gdb_func.go | Threads OmitZero through formatWhereHolder/formatWhereKeyValue and adds omit checks. |
| contrib/drivers/mysql/mysql_z_unit_model_test.go | Adds MySQL integration tests for OmitZero* and builder inheritance. |
Comments suppressed due to low confidence (1)
contrib/drivers/mysql/mysql_z_unit_model_test.go:1758
RowsAffected()error is currently discarded (n, _ := ...). Please assert/handle the error so test failures aren’t masked.
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
| 方法 | 判断目标 |
|---|---|
OmitNil |
值是否为nil |
OmitZero |
值是否为其 Go 类型的零值 |
OmitEmpty |
值的内容是否为空 |
最关键的两个反例是:
make([]string, 0):是空值,但不是零值。
[16]byte{}:是零值,但不是空值。
因此,OmitZero不是OmitNil或OmitEmpty的别名,而是将 Go 的零值语义引入gdb。
Go 本身要求开发者区分零值和空值。Go 1.24在encoding/json中增加omitzero标签,进一步明确了这种语义边界。标准库也提供了reflect.Value.IsZero(),自定义类型还可以通过IsZero() bool定义自己的零值规则。因此,不能笼统地使用指针或空值判断代替零值语义。
案例一:清空 JSON 数组
type UserUpdate struct {
LoginCount int `orm:"login_count"`
Tags []string `orm:"tags"`
}
input := UserUpdate{
Tags: make([]string, 0), // 明确清空标签
}
_, err := db.Model("users").
Where("id", userID).
OmitZeroData().
Data(input).
Update()业务意图是忽略默认的LoginCount=0,但将Tags=[]写入数据库:
| 方法 | 结果 |
|---|---|
OmitNilData |
同时保留两者,可能误写LoginCount=0 |
OmitEmptyData |
同时过滤两者,无法清空标签 |
OmitZeroData |
过滤LoginCount=0,保留Tags=[] |
这个案例证明:空值不一定是零值。
案例二:过滤零 UUID
UUID通常由固定长度的[16]byte表示:
type UUID [16]byte
var id UUID
users, err := db.Model("users").
OmitZeroWhere().
Where("uuid", id).
All()此时id是全零 UUID:
00000000-0000-0000-0000-000000000000
UUID{}是类型零值,但其数组长度为16,并不是空集合:
| 方法 | 是否过滤零 UUID |
|---|---|
OmitNilWhere |
否,UUID 是值类型 |
OmitEmptyWhere |
否,数组长度不为0 |
OmitZeroWhere |
是,UUID{}是类型零值 |
这个案例证明:零值也不一定是空值。
案例三:过滤零时间
time.Time通过IsZero()明确提供零值判断:
var createdAt time.Time
orders, err := db.Model("orders").
OmitZeroWhere().
Where("created_at", createdAt).
All()OmitZeroWhere可以过滤time.Time{},避免生成无意义的零时间条件。
当前OmitEmptyWhere也能识别零时间,但这是对时间类型的专门支持。OmitZeroWhere则通过统一的零值协议表达调用意图,语义更加直接。
案例四:自定义IsZero
业务类型可以自行定义零值:
type AccountID string
func (id AccountID) IsZero() bool {
return id == "" || id == "UNASSIGNED"
}
accountID := AccountID("UNASSIGNED")
records, err := db.Model("account_records").
OmitZeroWhere().
Where("account_id", accountID).
All()accountID既不是nil,底层字符串也不为空,但其IsZero()返回true:
| 方法 | 是否过滤UNASSIGNED |
|---|---|
OmitNilWhere |
否 |
OmitEmptyWhere |
否 |
OmitZeroWhere |
是 |
借助IsZero(),UUID、金额、版本号和业务 ID 等类型可以定义自己的零值,无需让gdb硬编码具体类型规则。
API 心智负担对比
从行为可预测性看,三种 API 的心智负担并不相同:
| API | 规则来源 | 心智负担 |
|---|---|---|
OmitNil |
Go 的nil语义 |
最低,判断边界明确 |
OmitZero |
Go 类型零值、reflect.Value.IsZero和类型的IsZero() |
较低,规则由类型负责 |
OmitEmpty |
框架对“空内容”的组合判断 | 最高,需要记忆框架规则 |
OmitEmpty中的“空”不是 Go 语言定义的统一状态。使用者需要额外了解以下值是否会被过滤:
nil、0、false和""。- 长度为
0的切片、映射和通道。 - 字段都为空的结构体。
- 时间类型及其他特殊类型。
相比之下,OmitNil回答“值是否存在”,OmitZero回答“值是否处于该类型的初始状态”。这两个问题都有明确的 Go 语言边界。OmitEmpty回答的则是“框架是否认为该值没有有效内容”,需要开发者同时理解类型状态和框架策略。
这种心智负担在WHERE条件中还会转化为实际风险:
ids := make([]int, 0)
users, err := db.Model("users").
OmitEmptyWhere().
Where("id", ids).
All()调用方可能认为空 ID 集合表示“不匹配任何用户”,但OmitEmptyWhere会把空切片和整个条件一起删除,查询可能退化为全表查询。OmitZeroWhere则会保留这个非零空切片,使其继续表达空集合条件。
因此,从低到高可以概括为:
OmitNil < OmitZero < OmitEmpty
新增OmitZero表面上增加了一个 API,实际上拆分了原本被OmitEmpty混在一起的“类型零值”和“内容为空”两种语义。调用方可以明确选择需要的过滤规则,不必依赖复杂的空值判断,因此整体上降低了认知成本和误用风险。
使用边界
零值不一定表示“没有提供”。false和0也可能是合法业务值。需要区分“未提供”和“明确传入零值”时,应使用指针或其他可选值类型:
type UserUpdate struct {
Enabled *bool
Count *int
}因此,OmitZero应保持显式、按需启用,而不应成为默认过滤行为。
最终结论
非
nil空数组证明了空值不一定是零值,全零 UUID 证明了零值也不一定是空值,time.Time.IsZero和自定义IsZero()则说明零值是类型可以正式声明的行为。三者相比,OmitEmpty才是规则最宽泛、心智负担最高的 API。新增OmitZero不是制造重复概念,而是将 Go 已有的零值语义从框架的空值策略中明确拆分出来。它既提高了 API 的可预测性,也避免空集合条件被错误删除等实际风险,因此属于正确性能力,而不是重复的便利 API。
Reverts #4818
源PR #4713 因为合并回滚了,没法重新打开,顾再整了一个回滚中的回滚