Golang 更新数据库字段为类型零值的三种方式(go-pg)

Published: 2023-02-14

Tags: Golang

本文总阅读量

之前使用 go-pg ORM 更新数据库字段为 Go 语言中零值时无法更新,当时使用 ugly 代码临时解决。今天翻看 go-pg 的文档并写了代码进行测试。

示例 SQL

CREATE TABLE IF NOT EXISTS users
(
    pk                VARCHAR PRIMARY KEY NOT NULL,
    name            VARCHAR NOT NULL,
    active            BOOLEAN DEFAULT 'f' NOT NULL,
    updated_at     TIMESTAMPTZ DEFAULT NOW() NOT NULL,
    created_at      TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

INSERT INTO "public"."users"("pk", "name", "active", "updated_at", "created_at") VALUES ('U001', 'dong', 't', '2023-02-13 11:54:31.153283+00', '2023-02-13 11:54:31.153283+00');

Golang 使用 go-pg 访问数据库

main.go

package main

import (
    "context"
    "fmt"
    "github.com/go-pg/pg/v10"
    "time"
)

var TestDB *pg.DB

type Users struct {
    tableName struct{}  `pg:"users,alias:g,discard_unknown_columns"`
    PK        string    `pg:"pk,pk"`
    Name      string    `pg:"name"`
    Active    bool      `pg:"active"`
    CreatedAt time.Time `pg:"created_at"`
    UpdatedAt time.Time `pg:"updated_at"`
}

func (U *Users) UpdateName(db *pg.DB) (int, error) {
    users := Users{
        PK: U.PK,
    }
    res, err := db.Model(&users).Set("name = ?", U.Name).WherePK().Update()
    return res.RowsAffected(), err
}

func init() {

    opt, err := pg.ParseURL("postgres://user:pass@localhost:5432/db_name")
    if err != nil {
        panic(err)
    }

    db := pg.Connect(opt)

    var version string
    ctx := context.Background()
    _, err = db.QueryOneContext(ctx, pg.Scan(&version), "SELECT version()")
    if err != nil {
        panic(err)
    }

    TestDB = db
    fmt.Println(version)
}

func main() {
    user := &Users{
        PK:   "U001",
        Name: "dong-updated",
    }
    user.UpdateName(TestDB)
}

运行后,数据库字段 name 值会被更新为 "dong-updated",通过 Set 更新单独的字段都没什么问题。也可以将 name 改为空字符串,或者将 active 改为 false。

如果我们想要使用 Update() 方法同时更新多个字段,代码如下

func (U *Users) Update(db *pg.DB) (int, error) {
    users := Users{
        PK:     U.PK,
        Name:   U.Name,
        Active: U.Active,
    }
    res, err := db.Model(&users).WherePK().Update()
    return res.RowsAffected(), err
}

func main() {
    user := &Users{
        PK:      "U001",
        Name:  "dong-updated",
        Active: false,
    }
    user.Update(TestDB)
}

运行则会触发 Panic(panic: runtime error: invalid memory address or nil pointer dereference)

error: null value in column "active" violates not-null constraint

数据库提示 active 不能为类型 NULL 值,从更详细的 Debug 信息可以看到 go-pg 没有将 go 零值赋值为 False,而是忽略了字段的处理,最终的 SQL 字段会被赋值为 null

Failing row contains (U001, dong-updated, null, null, null).

如果 Go 零值字段不赋值到数据库符合预期,而不想数据库报错,可以使用 UpdateNotZero() 替代 Update() 方法,只更新有值的字段。

通过以下方式,可以更新数据库字段为 Go 类型零值。

1)将 Struct 定义处的类型修改为 *bool

使用 *bool 类型,go-pg 能够识别出 active 为 False 是我们需要更新的数值,同时搭配 UpdateNotZero() 方法,可以忽略掉 Users 结构中未赋值的字段。


type Users struct {
     // ...
    Active    *bool     `pg:"active"`
}

func (U *Users) Update(db *pg.DB) (int, error) {
    // 使用 UpdateNotZero 忽略掉 NULL 值
    res, err := db.Model(U).WherePK().UpdateNotZero()
    return res.RowsAffected(), err
}

func BoolPointer(b bool) *bool {
    v := new(bool)
    *v = b
    return v
}

func main() {
    user := &Users{
        PK:     "U001",
        Active: BoolPointer(false),
    }
    user.Update(TestDB)
}

2)拼接 Set 构造 SQL 语句

手动构造 Set 的方式同样可以更新 active 为 False 值。

func (U *Users) Update(db *pg.DB) (int, error) {

    var setArr []string
    setArr = append(setArr, fmt.Sprintf("active='%v'", U.Active))
    setArr = append(setArr, fmt.Sprintf("name='%v'", U.Name))
    set := strings.Join(setArr, ",")

    res, err := db.Model(U).Set(set).WherePK().UpdateNotZero()
    return res.RowsAffected(), err
}

3)借助 V10 版本新支持的 map 结构

go-pg v10 版本新增通过 map 方式更新数据,使用方式如下,同样能达到更新字段为 False 值的效果。

func (U *Users) Update(db *pg.DB) (int, error) {

    values := map[string]interface{}{
        "name":  U.Name,
        "active": U.Active,
    }
    res, err := db.Model(&values).TableExpr("users").Where("pk = ?", U.PK).Update()
    return res.RowsAffected(), err
}

总结

通过 go-pg 更新 Go 零值到数据库时,修改字段为指针类型是最简便的,第二种拼接 Set 的方式简单粗暴,而在不修改字段为指针类型的前提下,第三种 map 是比拼接 Set 更优雅的方式。

参考