在使用 go-sqlbuilder
库进行 SQL 插入操作时,如果结构体中包含指针类型字段且部分字段为 nil
,这些 nil
字段会被插入为数据库的 NULL
值。以下是完整解决方案:
问题分析
- 默认行为:
go-sqlbuilder
默认会将所有结构体字段(包括nil
指针)包含在 INSERT 语句中nil
指针会被转换为 SQL 的NULL
- 典型场景:
type User struct {
ID *int64 `db:"id"`
Name *string `db:"name"`
Email *string `db:"email"`
}
user := User{
ID: ptr.Int64(1), // 赋值
Name: ptr.String("Alice"), // 赋值
// Email 为 nil
}
// 生成的 SQL:
// INSERT INTO users (id, name, email) VALUES (1, 'Alice', NULL)
解决方案
方法1:使用 Omit
选项(推荐)
package main
import (
"fmt"
"log"
sb "github.com/huandu/go-sqlbuilder"
)
type User struct {
ID *int64 `db:"id"`
Name *string `db:"name"`
Email *string `db:"email"`
Address *string `db:"address"`
}
func main() {
user := User{
ID: ptr.Int64(1),
Name: ptr.String("Alice"),
Email: nil, // 未设置
}
// 创建插入构建器
ib := sb.NewInsertBuilder()
ib.InsertInto("users")
// 自动检测非 nil 字段
fields := []string{}
values := []interface{}{}
// 使用反射检测非 nil 字段
v := reflect.ValueOf(user)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
tag := t.Field(i).Tag.Get("db")
// 跳过未设置字段
if field.IsNil() || tag == "" {
continue
}
fields = append(fields, tag)
values = append(values, field.Elem().Interface())
}
// 设置列和值
ib.Cols(fields...)
ib.Values(values...)
// 生成 SQL
sql, args := ib.Build()
fmt.Println("SQL:", sql)
fmt.Println("Args:", args)
// 输出:
// SQL: INSERT INTO users (id, name) VALUES (?, ?)
// Args: [1 Alice]
}
// 辅助函数:创建指针
func ptr[T any](v T) *T {
return &v
}
方法2:使用自定义标记(如 omitempty
)
type User struct {
ID *int64 `db:"id,omitempty"` // 添加 omitempty 标记
Name *string `db:"name,omitempty"`
Email *string `db:"email,omitempty"`
Address *string `db:"address,omitempty"`
}
func buildInsert(table string, data interface{}) (string, []interface{}) {
ib := sb.NewInsertBuilder()
ib.InsertInto(table)
v := reflect.ValueOf(data).Elem()
t := v.Type()
fields := []string{}
values := []interface{}{}
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
tag := t.Field(i).Tag.Get("db")
if tag == "" {
continue
}
// 解析 omitempty 标记
tagParts := strings.Split(tag, ",")
tagName := tagParts[0]
omitempty := false
if len(tagParts) > 1 && strings.Contains(tag, "omitempty") {
omitempty = true
}
// 跳过 nil 且标记了 omitempty 的字段
if omitempty && field.IsNil() {
continue
}
fields = append(fields, tagName)
if field.IsNil() {
values = append(values, nil)
} else {
values = append(values, field.Elem().Interface())
}
}
ib.Cols(fields...)
ib.Values(values...)
return ib.Build()
}
// 使用示例
func main() {
user := User{
ID: ptr.Int64(1),
Name: ptr.String("Alice"),
}
sql, args := buildInsert("users", &user)
fmt.Println("SQL:", sql) // INSERT INTO users (id, name) VALUES (?, ?)
}
方法3:使用 sqlbuilder.Struct
并过滤字段(库原生支持)
func main() {
user := User{
ID: ptr.Int64(1),
Name: ptr.String("Alice"),
Email: nil,
}
ib := sb.NewInsertBuilder()
ib.InsertInto("users")
// 创建字段映射
fieldMap := make(map[string]interface{})
if user.ID != nil {
fieldMap["id"] = *user.ID
}
if user.Name != nil {
fieldMap["name"] = *user.Name
}
if user.Email != nil {
fieldMap["email"] = *user.Email
}
if user.Address != nil {
fieldMap["address"] = *user.Address
}
// 添加字段
for col, val := range fieldMap {
ib.Col(col)
ib.Values(val)
}
sql, args := ib.Build()
fmt.Println("SQL:", sql)
fmt.Println("Args:", args)
}
方法4:扩展 sqlbuilder
添加 OmitNil
功能
// InsertBuilderWithOmit 扩展 InsertBuilder
type InsertBuilderWithOmit struct {
*sb.InsertBuilder
}
func NewInsertBuilderWithOmit() *InsertBuilderWithOmit {
return &InsertBuilderWithOmit{
InsertBuilder: sb.NewInsertBuilder(),
}
}
// AddStruct 添加结构体并忽略 nil 指针
func (ib *InsertBuilderWithOmit) AddStruct(data interface{}) {
v := reflect.ValueOf(data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
tag := t.Field(i).Tag.Get("db")
if tag == "" || field.IsNil() {
continue
}
ib.Col(tag)
ib.Values(field.Elem().Interface())
}
}
// 使用示例
func main() {
user := User{
ID: ptr.Int64(1),
Name: ptr.String("Alice"),
}
ib := NewInsertBuilderWithOmit()
ib.InsertInto("users")
ib.AddStruct(user)
sql, args := ib.Build()
fmt.Println("SQL:", sql) // INSERT INTO users (id, name) VALUES (?, ?)
}
完整解决方案(生产环境推荐)
package main
import (
"fmt"
"reflect"
"strings"
sb "github.com/huandu/go-sqlbuilder"
)
// User 示例结构体
type User struct {
ID *int64 `db:"id,omitempty"`
Name *string `db:"name,omitempty"`
Email *string `db:"email,omitempty"`
Address *string `db:"address,omitempty"`
}
// SafeInsertBuilder 安全的插入构建器
type SafeInsertBuilder struct {
*sb.InsertBuilder
table string
}
// NewSafeInsertBuilder 创建安全插入构建器
func NewSafeInsertBuilder(table string) *SafeInsertBuilder {
return &SafeInsertBuilder{
InsertBuilder: sb.NewInsertBuilder(),
table: table,
}
}
// AddStruct 添加结构体,自动忽略 nil 指针和空标记字段
func (b *SafeInsertBuilder) AddStruct(data interface{}) error {
v := reflect.ValueOf(data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return fmt.Errorf("expected struct, got %T", data)
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
// 获取 db 标签
tag := fieldType.Tag.Get("db")
if tag == "" {
continue
}
// 解析标签选项
tagParts := strings.Split(tag, ",")
colName := tagParts[0]
omitempty := false
for _, opt := range tagParts[1:] {
if opt == "omitempty" {
omitempty = true
break
}
}
// 检查是否应忽略
if omitempty && field.IsNil() {
continue
}
// 添加列和值
b.Col(colName)
if field.IsNil() {
b.Values(nil)
} else {
b.Values(field.Elem().Interface())
}
}
return nil
}
// Build 构建最终 SQL
func (b *SafeInsertBuilder) Build() (string, []interface{}) {
b.InsertBuilder.InsertInto(b.table)
return b.InsertBuilder.Build()
}
func main() {
// 创建测试用户
user := User{
ID: ptr.Int64(1),
Name: ptr.String("Alice"),
Email: nil, // 未设置
Address: ptr.String(""), // 空字符串
}
// 构建插入语句
builder := NewSafeInsertBuilder("users")
if err := builder.AddStruct(user); err != nil {
panic(err)
}
sql, args := builder.Build()
fmt.Println("SQL:", sql)
fmt.Println("Args:", args)
// 输出:
// SQL: INSERT INTO users (id, name) VALUES (?, ?)
// Args: [1 Alice]
}
// 辅助函数:创建指针
func ptr[T any](v T) *T {
return &v
}
处理特殊情况
- 处理空字符串:
// 在 AddStruct 方法中添加
if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.String {
if field.Elem().String() == "" && omitempty {
continue
}
}
- 处理零值:
// 添加零值检查
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Ptr:
return v.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.String:
return v.String() == ""
// 添加其他类型...
default:
return false
}
}
// 在 AddStruct 中使用
if omitempty && isZero(field) {
continue
}
- 处理嵌套结构:
// 递归处理嵌套结构
if field.Kind() == reflect.Struct {
if err := b.AddStruct(field.Addr().Interface()); err != nil {
return err
}
}
最佳实践建议
- 统一使用指针字段:
- 在可能为空的字段上使用指针类型
- 添加
omitempty
标签
- 添加辅助函数:
func PtrInt64(v int64) *int64 { return &v }
func PtrString(v string) *string { return &v }
- 数据库设计优化:
- 设置合理的默认值(
DEFAULT ''
) - 允许 NULL 的字段明确标记
- 日志记录:
func (b *SafeInsertBuilder) Build() (string, []interface{}) {
sql, args := b.InsertBuilder.Build()
log.Printf("Generated SQL: %s\nArgs: %v", sql, args)
return sql, args
}
- 单元测试:
func TestInsertBuilder(t *testing.T) {
tests := []struct {
name string
input User
expected string
}{
{"All fields", User{ID: PtrInt64(1), Name: PtrString("A")}, "INSERT INTO users (id, name) VALUES (?, ?)"},
{"With email", User{ID: PtrInt64(1), Email: PtrString("a@b.com")}, "INSERT INTO users (id, email) VALUES (?, ?)"},
{"Nil fields", User{}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := NewSafeInsertBuilder("users")
builder.AddStruct(tt.input)
sql, _ := builder.Build()
if sql != tt.expected {
t.Errorf("Expected %q, got %q", tt.expected, sql)
}
})
}
}
总结
通过以上方法,你可以确保:
nil
指针字段不会参与 SQL 插入- 使用
omitempty
标签控制字段行为 - 空字符串和零值可以根据需要处理
- 保持代码整洁且类型安全
对于生产环境,推荐使用 SafeInsertBuilder
封装方案,它提供了最佳的控制和错误处理机制,同时保持与 go-sqlbuilder
的兼容性。