Golang中如何序列化SQL类型

Golang中如何序列化SQL类型 我正在开始学习Go语言。为此,我基于Postgres(存储过程/函数)创建了一个小型REST-API,它本身运行良好。

然而,我注意到“标准编组器”生成的JSON输出与我预期的略有不同。

下面你可以看到我在所有服务中使用的模型(结构体):

type Aufgabe struct {

    ID             int             `json:"id"`

    Beschreibung   string          `json:"beschreibung"`

    StatusCode     int             `json:"statusCode"`

    StatusText     string          `json:"statusText"` // nullable, may be not present

    KategorieCode  int             `json:"kategorieCode"`

    KategorieText  string          `json:"kategorieText"` // nullable, may be not present

    ErstelltAm     time.Time       `json:"erstelltAmTS"`

    ZuErledigenBis sql.NullTime    `json:"zuErledigenBisTS"` // nullable, may be not present, Date

    Kosten         sql.NullFloat64 `json:"kosten"`           // nullable, may be not present

    DauerMin       sql.NullInt32   `json:"dauerMin"`         // nullable, may be not present

    Wichtig        bool            `json:"wichtig"`

    ErledigtAm     sql.NullTime    `json:"erledigtAmTS"` // nullable, may be not present

}

输出结果如下:

{

    "id": 5,

    "beschreibung": "sdsd",

    "statusCode": 2,

    "statusText": "gelöscht",

    "kategorieCode": 2,

    "kategorieText": "einkauf",

    "erstelltAmTS": "2020-09-15T18:45:39.974309Z",

    "zuErledigenBisTS": {

        "Time": "2020-09-22T00:00:00Z",

        "Valid": true

    },

    "kosten": {

        "Float64": 0,

        "Valid": false

    },

    "dauerMin": {

        "Int32": 0,

        "Valid": false

    },

    "wichtig": false,

    "erledigtAmTS": {

        "Time": "0001-01-01T00:00:00Z",

        "Valid": false

    }

}

显然,这些“SQL类型”本身是对象。我希望只返回实际值,并且可能还需要格式化时间戳。

这里有一个使用node.js生成的相同结构的示例:

{

    "id": 5,

    "beschreibung": "sdsd",

    "statusCode": 2,

    "statusText": "gelöscht",

    "kategorieCode": 2,

    "erstelltTS": "2020-09-15T16:45:39.974Z",

    "kategorieText": "einkauf",

    "kosten": null,

    "dauerMin": null,

    "wichtig": false,

    "erledigtAmTS": null

}

你可以看到它只包含实际值,并为空值打印null。

我在Go版本中遇到的另一个问题是,“添加/创建”服务也期望这些对象类型的JSON结构,如果客户端只提供了值,则无法映射实际的sql类型。

换句话说:在Golang中处理JSON和数据库的最佳实践是什么?我读过关于自定义编组器的内容,但这对我来说太复杂了,难以理解。也许我应该使用另一个库?


更多关于Golang中如何序列化SQL类型的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

omsec:

你可能看到它只包含实际值,并为空值打印 null。

切换到库 GitHub - guregu/null: reasonable handling of nullable values 并使用 null.Time 代替 sql.NullTime,使用 null.Float 代替 sql.NullFloat64。它会在 JSON 中将 NULL 值编码为 null

omsec:

我在 Go 版本中遇到的另一个问题是,“添加/创建”服务也期望那些对象类型的 JSON 结构,如果客户端只提供了值,则无法映射实际的 sql 类型。

我不太明白你在这里想表达什么,你能提供一些例子来说明你的意思吗。

更多关于Golang中如何序列化SQL类型的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中处理SQL可空类型与JSON序列化的标准方法是实现自定义的MarshalJSONUnmarshalJSON方法。以下是针对你的Aufgabe结构体的完整解决方案:

import (
    "database/sql"
    "encoding/json"
    "time"
)

type Aufgabe struct {
    ID             int             `json:"id"`
    Beschreibung   string          `json:"beschreibung"`
    StatusCode     int             `json:"statusCode"`
    StatusText     string          `json:"statusText"`
    KategorieCode  int             `json:"kategorieCode"`
    KategorieText  string          `json:"kategorieText"`
    ErstelltAm     time.Time       `json:"erstelltAmTS"`
    ZuErledigenBis sql.NullTime    `json:"zuErledigenBisTS"`
    Kosten         sql.NullFloat64 `json:"kosten"`
    DauerMin       sql.NullInt32   `json:"dauerMin"`
    Wichtig        bool            `json:"wichtig"`
    ErledigtAm     sql.NullTime    `json:"erledigtAmTS"`
}

// MarshalJSON 自定义JSON序列化
func (a Aufgabe) MarshalJSON() ([]byte, error) {
    type Alias Aufgabe // 创建别名避免递归调用
    
    // 处理可空字段
    var zuErledigenBis interface{}
    if a.ZuErledigenBis.Valid {
        zuErledigenBis = a.ZuErledigenBis.Time.Format(time.RFC3339)
    } else {
        zuErledigenBis = nil
    }
    
    var kosten interface{}
    if a.Kosten.Valid {
        kosten = a.Kosten.Float64
    } else {
        kosten = nil
    }
    
    var dauerMin interface{}
    if a.DauerMin.Valid {
        dauerMin = a.DauerMin.Int32
    } else {
        dauerMin = nil
    }
    
    var erledigtAm interface{}
    if a.ErledigtAm.Valid {
        erledigtAm = a.ErledigtAm.Time.Format(time.RFC3339)
    } else {
        erledigtAm = nil
    }
    
    // 创建临时结构体进行序列化
    aux := &struct {
        *Alias
        ZuErledigenBis interface{} `json:"zuErledigenBisTS"`
        Kosten         interface{} `json:"kosten"`
        DauerMin       interface{} `json:"dauerMin"`
        ErledigtAm     interface{} `json:"erledigtAmTS"`
        ErstelltAm     string      `json:"erstelltAmTS"`
    }{
        Alias:          (*Alias)(&a),
        ZuErledigenBis: zuErledigenBis,
        Kosten:         kosten,
        DauerMin:       dauerMin,
        ErledigtAm:     erledigtAm,
        ErstelltAm:     a.ErstelltAm.Format(time.RFC3339),
    }
    
    return json.Marshal(aux)
}

// UnmarshalJSON 自定义JSON反序列化
func (a *Aufgabe) UnmarshalJSON(data []byte) error {
    type Alias Aufgabe
    aux := &struct {
        ZuErledigenBis *string  `json:"zuErledigenBisTS"`
        Kosten         *float64 `json:"kosten"`
        DauerMin       *int32   `json:"dauerMin"`
        ErledigtAm     *string  `json:"erledigtAmTS"`
        ErstelltAm     string   `json:"erstelltAmTS"`
        *Alias
    }{
        Alias: (*Alias)(a),
    }
    
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    
    // 处理时间字段
    if t, err := time.Parse(time.RFC3339, aux.ErstelltAm); err == nil {
        a.ErstelltAm = t
    }
    
    // 处理可空时间字段
    if aux.ZuErledigenBis != nil {
        if t, err := time.Parse(time.RFC3339, *aux.ZuErledigenBis); err == nil {
            a.ZuErledigenBis = sql.NullTime{Time: t, Valid: true}
        }
    }
    
    if aux.ErledigtAm != nil {
        if t, err := time.Parse(time.RFC3339, *aux.ErledigtAm); err == nil {
            a.ErledigtAm = sql.NullTime{Time: t, Valid: true}
        }
    }
    
    // 处理可空数值字段
    if aux.Kosten != nil {
        a.Kosten = sql.NullFloat64{Float64: *aux.Kosten, Valid: true}
    }
    
    if aux.DauerMin != nil {
        a.DauerMin = sql.NullInt32{Int32: *aux.DauerMin, Valid: true}
    }
    
    return nil
}

使用示例:

// 序列化示例
func main() {
    aufgabe := Aufgabe{
        ID:            5,
        Beschreibung:  "sdsd",
        StatusCode:    2,
        StatusText:    "gelöscht",
        KategorieCode: 2,
        KategorieText: "einkauf",
        ErstelltAm:    time.Now(),
        ZuErledigenBis: sql.NullTime{
            Time:  time.Date(2020, 9, 22, 0, 0, 0, 0, time.UTC),
            Valid: true,
        },
        Kosten:   sql.NullFloat64{Valid: false},
        DauerMin: sql.NullInt32{Valid: false},
        Wichtig:  false,
        ErledigtAm: sql.NullTime{Valid: false},
    }
    
    // 序列化为JSON
    jsonData, err := json.Marshal(aufgabe)
    if err != nil {
        panic(err)
    }
    
    fmt.Println(string(jsonData))
    // 输出:
    // {
    //     "id": 5,
    //     "beschreibung": "sdsd",
    //     "statusCode": 2,
    //     "statusText": "gelöscht",
    //     "kategorieCode": 2,
    //     "kategorieText": "einkauf",
    //     "erstelltAmTS": "2020-09-15T18:45:39.974309Z",
    //     "zuErledigenBisTS": "2020-09-22T00:00:00Z",
    //     "kosten": null,
    //     "dauerMin": null,
    //     "wichtig": false,
    //     "erledigtAmTS": null
    // }
    
    // 反序列化示例
    jsonStr := `{
        "id": 5,
        "beschreibung": "sdsd",
        "statusCode": 2,
        "statusText": "gelöscht",
        "kategorieCode": 2,
        "kategorieText": "einkauf",
        "erstelltAmTS": "2020-09-15T18:45:39.974309Z",
        "zuErledigenBisTS": "2020-09-22T00:00:00Z",
        "kosten": null,
        "dauerMin": 30,
        "wichtig": false,
        "erledigtAmTS": null
    }`
    
    var aufgabe2 Aufgabe
    if err := json.Unmarshal([]byte(jsonStr), &aufgabe2); err != nil {
        panic(err)
    }
    
    fmt.Printf("DauerMin: %v (Valid: %v)\n", aufgabe2.DauerMin.Int32, aufgabe2.DauerMin.Valid)
    // 输出:DauerMin: 30 (Valid: true)
}

这个实现确保了:

  1. 序列化时,有效的SQL可空字段输出实际值,无效字段输出null
  2. 所有时间字段都格式化为RFC3339格式
  3. 反序列化时,可以正确解析客户端发送的JSON数据
  4. 保持了与数据库的兼容性,可以直接使用sql.Null*类型进行数据库操作
回到顶部