Skip to content

空值类型

VEF Framework 提供了一组空值类型(Nullable Types),用于处理数据库中的可空字段。

为什么需要空值类型?

Go 的基本类型(如 stringintbool)不能表示 NULL 值。当数据库字段可以为 NULL 时,需要使用特殊的类型来区分"空值"和"NULL"。

go
// Problem: Can't distinguish between empty string and NULL
type User struct {
    Bio string  // "" could mean empty or NULL
}

// Solution: Use null.String
type User struct {
    Bio null.String  // Can represent "", NULL, or actual value
}

可用的空值类型

VEF Framework 在 github.com/ilxqx/vef-framework-go/null 包中提供以下空值类型:

类型对应基本类型说明
null.Stringstring可空字符串
null.Intint可空整数
null.Int64int64可空 64 位整数
null.Float64float64可空浮点数
null.Boolbool可空布尔值
null.Timetime.Time可空时间

基本用法

导入包

go
import "github.com/ilxqx/vef-framework-go/null"

在模型中使用

go
type User struct {
    orm.Model
    
    // Required fields (not nullable)
    Username string `bun:"username,notnull" json:"username"`
    Email    string `bun:"email,notnull" json:"email"`
    
    // Optional fields (nullable)
    DisplayName null.String  `bun:"display_name" json:"displayName"`
    Bio         null.String  `bun:"bio" json:"bio"`
    Age         null.Int     `bun:"age" json:"age"`
    Score       null.Float64 `bun:"score" json:"score"`
    IsVerified  null.Bool    `bun:"is_verified" json:"isVerified"`
    BirthDate   null.Time    `bun:"birth_date" json:"birthDate"`
}

创建空值类型

从值创建

go
// Create from value
name := null.StringFrom("John")           // Valid string
age := null.IntFrom(25)                   // Valid int
score := null.Float64From(95.5)           // Valid float
active := null.BoolFrom(true)             // Valid bool
date := null.TimeFrom(time.Now())         // Valid time

创建 NULL 值

go
// Create NULL values
name := null.String{}                     // NULL string
age := null.Int{}                         // NULL int
score := null.Float64{}                   // NULL float
active := null.Bool{}                     // NULL bool
date := null.Time{}                       // NULL time

从指针创建

go
// Create from pointer (nil pointer = NULL)
var namePtr *string = nil
name := null.StringFromPtr(namePtr)       // NULL

value := "John"
name := null.StringFromPtr(&value)        // Valid string "John"

读取值

检查是否有效

go
user := User{
    DisplayName: null.StringFrom("John"),
    Age:         null.Int{},  // NULL
}

// Check if value is valid (not NULL)
if user.DisplayName.Valid {
    fmt.Println("Display name:", user.DisplayName.String)
}

if user.Age.Valid {
    fmt.Println("Age:", user.Age.Int)
} else {
    fmt.Println("Age is not set")
}

获取值或默认值

go
// Get value or zero value if NULL
name := user.DisplayName.ValueOrZero()    // "" if NULL
age := user.Age.ValueOrZero()             // 0 if NULL

// Custom default value pattern
func GetDisplayName(user User) string {
    if user.DisplayName.Valid {
        return user.DisplayName.String
    }
    return user.Username  // Fallback to username
}

转换为指针

go
// Convert to pointer (nil if NULL)
namePtr := user.DisplayName.Ptr()         // *string or nil
agePtr := user.Age.Ptr()                  // *int or nil

JSON 序列化

空值类型会正确序列化为 JSON:

go
type User struct {
    Name null.String `json:"name"`
    Age  null.Int    `json:"age"`
}

// Valid values
user := User{
    Name: null.StringFrom("John"),
    Age:  null.IntFrom(25),
}
// JSON: {"name": "John", "age": 25}

// NULL values
user := User{
    Name: null.String{},
    Age:  null.Int{},
}
// JSON: {"name": null, "age": null}

使用 omitempty

go
type User struct {
    Name null.String `json:"name,omitempty"`
    Age  null.Int    `json:"age,omitempty"`
}

// NULL values with omitempty are omitted
user := User{
    Name: null.String{},
    Age:  null.Int{},
}
// JSON: {}

在参数中使用

搜索参数

go
type UserSearch struct {
    api.P
    Username string     `json:"username" search:"contains"`
    Age      null.Int   `json:"age" search:"gte"`        // Optional filter
    IsActive null.Bool  `json:"isActive" search:"eq"`    // Optional filter
}

// Request with optional filters
// {
//     "resource": "app/sys/user",
//     "action": "find_page",
//     "params": {
//         "username": "john",
//         "age": 18,           // Filter by age >= 18
//         "isActive": null     // No filter on isActive
//     }
// }

更新参数

go
type UserUpdateParams struct {
    api.P
    Id          string      `json:"id"`
    DisplayName null.String `json:"displayName"`  // Can be set to NULL
    Age         null.Int    `json:"age"`          // Can be set to NULL
}

// Update with NULL value
// {
//     "resource": "app/sys/user",
//     "action": "update",
//     "params": {
//         "id": "user-001",
//         "displayName": null,  // Set to NULL
//         "age": 30             // Set to 30
//     }
// }

数据库操作

查询条件

go
// Query with nullable field
func FindUsersByAge(ctx context.Context, db orm.Db, minAge null.Int) ([]User, error) {
    query := db.NewSelect().Model(&[]User{})
    
    if minAge.Valid {
        query = query.Where(func(cb orm.ConditionBuilder) {
            cb.GreaterThanOrEquals("age", minAge.Int)
        })
    }
    
    var users []User
    err := query.Scan(ctx)
    return users, err
}

插入和更新

go
// Insert with nullable fields
user := User{
    Username:    "john",
    Email:       "john@example.com",
    DisplayName: null.StringFrom("John Doe"),  // Set value
    Age:         null.Int{},                    // NULL
}
_, err := db.NewInsert().Model(&user).Exec(ctx)

// Update nullable field to NULL
_, err = db.NewUpdate().
    Model(&User{}).
    Set("display_name = ?", null.String{}).  // Set to NULL
    Where(func(cb orm.ConditionBuilder) {
        cb.Equals("id", userId)
    }).
    Exec(ctx)

常见模式

可选配置

go
type AppConfig struct {
    orm.Model
    Key         string       `bun:"key,pk" json:"key"`
    StringValue null.String  `bun:"string_value" json:"stringValue"`
    IntValue    null.Int     `bun:"int_value" json:"intValue"`
    BoolValue   null.Bool    `bun:"bool_value" json:"boolValue"`
}

// Get config with default
func GetConfigString(ctx context.Context, db orm.Db, key string, defaultValue string) string {
    var config AppConfig
    err := db.NewSelect().
        Model(&config).
        Where(func(cb orm.ConditionBuilder) {
            cb.Equals("key", key)
        }).
        Scan(ctx)
    
    if err != nil || !config.StringValue.Valid {
        return defaultValue
    }
    return config.StringValue.String
}

可选关联

go
type Order struct {
    orm.Model
    CustomerId  string      `bun:"customer_id,notnull" json:"customerId"`
    CouponId    null.String `bun:"coupon_id" json:"couponId"`  // Optional coupon
    
    // Relations
    Customer    *User       `bun:"rel:belongs-to,join:customer_id=id" json:"customer,omitempty"`
    Coupon      *Coupon     `bun:"rel:belongs-to,join:coupon_id=id" json:"coupon,omitempty"`
}

软删除时间戳

go
type Article struct {
    orm.Model
    Title     string    `bun:"title,notnull" json:"title"`
    DeletedAt null.Time `bun:"deleted_at,soft_delete" json:"deletedAt,omitempty"`
}

下一步

基于 Apache License 2.0 许可发布