使用Gin+Gorm进行开发时的一些踩坑总结
前言
最近在使用Gin+Gorm进行运维集中化后端的开发,期间遇到一些问题,这里进行记录总结,希望也能帮到遇到同样问题的朋友。
正文
嵌套结构体绑定时缺失字段
这是一个实际开发中遇到的问题,为了兼容多种类型的数据库,定义了一个基础的数据库信息的结构体,如下:
// GeneralDB 基础数据库类型
type GeneralDB struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Database string `yaml:"database"`
}
针对PG数据库的话,会多出时区的配置,结构体要这样写:
type PostgreDB struct {
GeneralDB `mapstructure:",squash"`
TimeZone string `yaml:"time_zone"`
}
必须加上mapstructure:",squash"
的tag,如果不加,进行解析的时候,不会报错,但是绑定不到结构体上;
Gorm进行更新时没有反应
这里写了两种更新逻辑,一种是兼容软删除数据的更新方式,使用Save,一种是使用Updates进行兼容多列和单列的更新:
// UpdatePrometheus 更新Prometheus信息
//
// @receiver dao *PrometheusDao
// @param p monitor.Prometheus
// @return prometheus monitor.Prometheus, err error
func (dao *PrometheusDao) UpdatePrometheus(p monitor.Prometheus) (prometheus monitor.Prometheus, err error) {
session := global.DB.Session(&gorm.Session{QueryFields: true})
err = session.Unscoped().Save(&p).Error
if err != nil {
global.Logger.Debugf("数据库操作异常, 异常信息: %s.\n", err)
}
return p, err
}
// UpdataPrometheusMap 使用Map更新信息
//
// @receiver dao *PrometheusDao
// @param id uint, param map[string]interface{}
// @return prometheus monitor.Prometheus, err error
func (dao *PrometheusDao) UpdataPrometheusMap(id uint, param map[string]interface{}) (prometheus monitor.Prometheus, err error) {
session := global.DB.Session(&gorm.Session{QueryFields: true})
err = session.Model(&monitor.Prometheus{}).Where("id = ?", id).Updates(param).Error
if err != nil {
global.Logger.Debugf("数据库操作异常, 异常信息: %s.\n", err)
}
return prometheus, err
}
在Service层进行数据库方法的调用时,UpdatePrometheus
可以正常完成数据更新,但是UpdatePrometheusMap
不可以,经过查证,入参的类型必须是map[string]interface{}
,而我之前设置的map[string]string
Viper解析yaml配置
使用Viper包进行配置文件的解析,其中有部分配置文件需要程序运行过程中监听配置的修改,然后进行重载,在我的需求中,配置文件会有多个,所以就要有多个viper实例,对于这点官方文档也给了回答:
一个viper实例只能针对一个名称的配置文件进行监听和加载(但是支持多个路径和后缀),所以有几个配置就要有几个viper实例,因此,实现一个能够适用所有业务配置结构的初始化函数:
func initConfigFile(filename string, config interface{}) *viper.Viper {
v := viper.New()
// 设定配置读取路径,允许设置多个路径
v.AddConfigPath(global.RunPath)
// 设置配置文件名称,viper会分别去上面设置的几个路径下去寻找
v.SetConfigName(filename)
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
global.Logger.Errorf("Failed to load config file %s, %s.\n", filename, err.Error())
return v
}
// 开启配置文件监视,监听文件的变更,仅当文件保存时才会刷新
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
global.Logger.Infof("Config file changed: %s, will reload it.\n", filename)
if err := v.Unmarshal(&config); err != nil {
global.Logger.Errorf("Reload the config file failed, %s.\n", err.Error())
}
})
if err := v.Unmarshal(&config); err != nil {
global.Logger.Errorf("Load the config file failed, %s.\n", err.Error())
}
return v
}
这样就能实现配置的热加载更新了,如果是在windows开发,注意一点,那就是使用其他文本工具修改配置文件测试更新时,可能会产生2或4次刷新:
这个和Windows的系统调用有关,不用在意,win11用户直接使用自带的notepad打开就不会有这个问题了:
Gorm使用自定义时间类型
按照以下方式自定义一个LocalTime即可:
type LocalTime time.Time
func (t *LocalTime) MarshalJSON() ([]byte, error) {
tTime := time.Time(*t)
return []byte(fmt.Sprintf("\"%v\"", tTime.Format("2006-01-02 15:04:05"))), nil
}
func (t *LocalTime) Value() (driver.Value, error) {
var zero time.Time
tlt := time.Time(*t)
if tlt.UnixNano() == zero.UnixNano() {
return nil, nil
}
return tlt, nil
}
func (t *LocalTime) Scan(v any) error {
if value, ok := v.(time.Time); ok {
*t = LocalTime(value)
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
进行model的设置时,这样指定:
type HardModel struct {
ID uint `gorm:"primaryKey" json:"id"` // 主键ID
CreatedAt LocalTime `json:"create_at"` // 创建时间
UpdatedAt LocalTime `json:"update_at"` // 更新时间
}