• 周二. 7 月 1st, 2025

    go-sqlbuilder 库进行 SQL 插入操作时,如果结构体中包含指针类型字段且部分字段为 nil,这些 nil 字段会被插入为数据库的 NULL 值解决方法

    root

    6 月 27, 2025

    在使用 go-sqlbuilder 库进行 SQL 插入操作时,如果结构体中包含指针类型字段且部分字段为 nil,这些 nil 字段会被插入为数据库的 NULL 值。以下是完整解决方案:

    问题分析

    1. 默认行为
    • go-sqlbuilder 默认会将所有结构体字段(包括 nil 指针)包含在 INSERT 语句中
    • nil 指针会被转换为 SQL 的 NULL
    1. 典型场景
       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
    }

    处理特殊情况

    1. 处理空字符串
       // 在 AddStruct 方法中添加
       if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.String {
           if field.Elem().String() == "" && omitempty {
               continue
           }
       }
    1. 处理零值
       // 添加零值检查
       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
       }
    1. 处理嵌套结构
       // 递归处理嵌套结构
       if field.Kind() == reflect.Struct {
           if err := b.AddStruct(field.Addr().Interface()); err != nil {
               return err
           }
       }

    最佳实践建议

    1. 统一使用指针字段
    • 在可能为空的字段上使用指针类型
    • 添加 omitempty 标签
    1. 添加辅助函数
       func PtrInt64(v int64) *int64 { return &v }
       func PtrString(v string) *string { return &v }
    1. 数据库设计优化
    • 设置合理的默认值(DEFAULT ''
    • 允许 NULL 的字段明确标记
    1. 日志记录
       func (b *SafeInsertBuilder) Build() (string, []interface{}) {
           sql, args := b.InsertBuilder.Build()
           log.Printf("Generated SQL: %s\nArgs: %v", sql, args)
           return sql, args
       }
    1. 单元测试
       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)
                   }
               })
           }
       }

    总结

    通过以上方法,你可以确保:

    1. nil 指针字段不会参与 SQL 插入
    2. 使用 omitempty 标签控制字段行为
    3. 空字符串和零值可以根据需要处理
    4. 保持代码整洁且类型安全

    对于生产环境,推荐使用 SafeInsertBuilder 封装方案,它提供了最佳的控制和错误处理机制,同时保持与 go-sqlbuilder 的兼容性。

    root

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注