之前使用 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 更优雅的方式。