浅谈 GORM 默认值处理

6,577 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

在 gorm 使用的时候,很多同学会很纠结,发现默认值的更新总是不太对,明明数据库声明了 create_time,加上了 default 值,但最后插入 db 的却还是零值。

今天这篇,我们来看看在 gorm 中怎么使用默认值。

default 标签

type User struct {
  ID   int64
  Name string `gorm:"default:galeone"`
  Age  int64  `gorm:"default:18"`
}

gorm 支持这样的 default 标签,直接声明字段的默认值。这样在插入记录到数据库时,默认值就会被用于填充值为 零值 的字段。

比如上面的 Name,如果我们留空,在 gorm 生成 sql 时就会被转为 "galeone"。

这一点的支持在 schema.ParseField 里面,首先还是经典的用反射取 tag 的操作,拿到 gorm 对应的 tagSettings。我们要用的 default 就在其中。

tagSetting = ParseTagSetting(fieldStruct.Tag.Get("gorm"), ";")

func ParseTagSetting(str string, sep string) map[string]string {
	settings := map[string]string{}
	names := strings.Split(str, sep)

	for i := 0; i < len(names); i++ {
		j := i
		if len(names[j]) > 0 {
			for {
				if names[j][len(names[j])-1] == '\\' {
					i++
					names[j] = names[j][0:len(names[j])-1] + sep + names[i]
					names[i] = ""
				} else {
					break
				}
			}
		}

		values := strings.Split(names[j], ":")
		k := strings.TrimSpace(strings.ToUpper(values[0]))

		if len(values) >= 2 {
			settings[k] = strings.Join(values[1:], ":")
		} else if k != "" {
			settings[k] = k
		}
	}

	return settings
}

有了 tagSettings ,我们就可以找到 default 这个子标签,并拿到默认值:

if v, ok := field.TagSettings["DEFAULT"]; ok {
        field.HasDefaultValue = true
        field.DefaultValue = v
}

随后对默认值进行 trim,并用反射,转换 默认值的类型: image.png

最后在 ConvertToCreateValues 函数里,判断如果是零值,就用这里的 DefaultValueInterface 的值:

image.png

整体实现并不复杂,大家可以参照源码了解一下。

从一个开发者的角度,我们只需要记住一开始的例子,用 gorm:"default:galeone" 来传入默认值即可。

默认值为什么不生效

有些同学很费解,为什么我没有声明 default 标签,默认值就不生效了。

笔者自己就遇见过这种case,在 model 里我们定义了 int, bool 等基本类型,将其从 0 改为 1 时可以正常插入,但从 1 改为 0 就不生效了。

这一点很 trick,本质在于,golang 对于基础类型都是有零值的。也就意味着,当你没有给 struct 中的部分 field 赋值时,默认的零值会干扰判断。

到底是你刻意不去赋值,希望用 0,还是只是漏了传呢?

这样的语义是比较混乱的。所以 gorm 的哲学在于,对于零值,统一认为没有传,不会插入到数据库。

在更新数据时,如果使用了 struct 来更新数据,默认只会更新非零值字段,如果使用map更新数据,则会更新全部字段,在使用 struct 更新时,也可以使用 Select 方法来选择想要更新的字段,在这种情况下,零值/非零值字段都会更新.

对于声明了默认值的字段,像 0''false 等零值是不会保存到数据库。您需要使用指针类型或 Scanner/Valuer 来避免这个问题.

type User struct {
  gorm.Model
  Name string
  Age  *int           `gorm:"default:18"`
  Active sql.NullBool `gorm:"default:true"`
}

当我们声明为指针后,就可以和零值区分开了,毕竟指针的零值对应的是 nil。

// UPDATE users SET name='new_name', age=0 WHERE id=111;

DB.Model(&result).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})



// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34' WHERE id = 1

db.Model(&user).Updates(User{Name: "hello", Age: 0, Active: false})

如何用数据库的默认值

我有一个model,在db里面设置了默认值,怎么让gorm不要给这个字段在 insert 的时候设置go的 zero value

前面我们介绍过,尤其是对于 createTime, updateTime 这类字段,数据库里我们会声明 default 使用 current timestamp。

此时我们不希望用 gorm 默认的,而是希望它放过去,直接走到 DB,这时候又该怎么处理呢?

gorm 为此提供了一个 default:(-) 标签,加上它,当我们留零值时,gorm 不会额外处理,就可以使用 DB 默认的声明了。


type User struct {
  ID        string `gorm:"default:uuid_generate_v3()"` // db func
  FirstName string
  LastName  string
  Age       uint8
  FullName  string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
}