空值类型
VEF Framework 提供了一组空值类型(Nullable Types),用于处理数据库中的可空字段。
为什么需要空值类型?
Go 的基本类型(如 string、int、bool)不能表示 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.String | string | 可空字符串 |
null.Int | int | 可空整数 |
null.Int64 | int64 | 可空 64 位整数 |
null.Float64 | float64 | 可空浮点数 |
null.Bool | bool | 可空布尔值 |
null.Time | time.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 nilJSON 序列化
空值类型会正确序列化为 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"`
}