Skip to content

Commit

Permalink
Merge pull request #417 from itcuihao/haoc7
Browse files Browse the repository at this point in the history
Go、Gorm与MySQL中timestamp交互时遇到的问题
  • Loading branch information
yangwenmai committed Jun 19, 2019
2 parents f5897d5 + 4092079 commit f367942
Showing 1 changed file with 334 additions and 0 deletions.
334 changes: 334 additions & 0 deletions content/discuss/2019-06-19-gorm-mysql-timestamp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
---
title: 2019-06-19 GoGorm MySQL timestamp
date: 2019-06-19T00:00:00+08:00
---

来源:《Go 夜读微信群

----

GoGorm与MySQL中timestamp交互时遇到的问题

涉及到的方面

- MySQL中timestamp 默认值explicit_defaults_for_timestamp属性设置
- Go中time.Time字段类型time.Time零值
- Gorm中的处理方式


例如:

数据模型

```
type A struct {
Id int
UserId int
VipExpireTime time.Time
MessageType string
ClickTabTime time.Time
CreateTime time.Time `gorm:"default:current_time"`
UpdateTime time.Time `gorm:"default:current_time"`
}
```

对应字段

```
CREATE TABLE `a` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`vip_expire_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'vip终止时间',
`message_type` varchar(50) NOT NULL DEFAULT '' COMMENT '消息的类型',
`click_tab_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '点击Tab的时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='a表';
```

数据库初始化

```
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type DBOrm struct {
Orm *gorm.DB
}
var DB DBOrm
const (
dbTestHost = "127.0.0.1"
dbTestUser = "root"
dbTestPwd = "123"
dbDevDB = "test1"
)
func InitGorm(user, password, addr, db string) {
var err error
DB.Orm, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
user, password, addr, db))
if err != nil {
panic(err)
}
DB.Orm.LogMode(true)
}
func InitDebug() {
InitGorm(dbTestUser, dbTestPwd, dbTestHost, dbDevDB)
}
```

```
func timenull() {
InitDebug()
a := A{
UserId: 1,
// VipExpireTime: time.Time{},
}
err := DB.Orm.Table("a").Create(&a).Error
fmt.Println(err)
fmt.Printf("a: %+v", a)
}
```

执行结果
```
d:\mygo\src\ch\t>go test -v -run TestTimenull
=== RUN TestTimenull
?[35m(D:/mygo/src/ch/t/run.go:263)?[0m
?[33m[2019-06-18 16:41:16]?[0m ?[36;1m[1.01ms]?[0m INSERT INTO `a` (`user_id`,`vip_expire_time`,`message_type`,`click_tab_time`) VALUES (1,'0001-01-01 00:00:00','','0001-01-01 00:00:00')
?[36;31m[1 rows affected or returned ]?[0m
<nil>
a: {Id:3 UserId:1 VipExpireTime:0001-01-01 00:00:00 +0000 UTC MessageType: ClickTabTime:0001-01-01 00:00:00 +0000 UTC}--- PASS: TestTimenull (0.02s)
PASS
ok ch/t 0.266s
```

发现这样`gorm`中操作是可以创建成功的但是如果粘贴`insert`语句到数据库中执行是报错的

```
INSERT INTO `a` (`user_id`,`vip_expire_time`,`message_type`,`click_tab_time`) VALUES (1,'0001-01-01 00:00:00','','0001-01-01 00:00:00')
```

```
错误代码: 1292
Incorrect datetime value: '0001-01-01 00:00:00' for column 'vip_expire_time' at row 1
```

造成这种时间的原因是什么呢

大概是因为`Go`语言中`time`的初始值是`第一年的一月一日`这个设定
[Golang Time](https://golang.org/pkg/time/#Time)

想避免这种方式要怎么处理呢

也带着问题问在夜读群中讨论

杨文:@我的名字叫浩仔丶Go 请教一个关于gorm create的问题,结构体user内部有一个time.Time字段a对应数据库是timestamp类型create是如果没有对a赋值insert会报错插入时间为0001-01-01修改方案想了两种,一个是给a赋time.Time{},另一种是将a改为指针的time插入null这两种哪种好呢大家是怎么处理的呢
@jinzhu gorm 作者

jinzhu可以用 *time.Time或者类似 NullTime 这种类型

jinzhu:并且你的mysql应该是5.7之后的新版本吧有个变量允许 0001-01-01 这类数据。。。」

Gorm 作者提到的两种方式

- `*time.Time`这貌似也是gorm issue里大部分的答案
- `NullTime`

第一种方式

```
type A struct {
Id int
UserId int
VipExpireTime *time.Time
MessageType string
ClickTabTime *time.Time
}
a := A{
UserId: 1,
}
INSERT INTO `a` (`user_id`,`vip_expire_time`,`message_type`,`click_tab_time`) VALUES (1,NULL,'',NULL);
```

这样`gorm`中操作是报错的

```
Error Code: 1048. Column 'vip_expire_time' cannot be null
```

另外此时又会引入新的问题因为字段设置为指针类型所以再取值时需要判断是否为null否则会空指针

在每一个用到`VipExpireTime`的地方都需要判断

```
if VipExpireTime != nil { VipExpireTime.Format("2006-01-02 15:04:05") }
```

这种代码让人头大

再有Go Time的定义也不建议用*time.Time

>Programs using times should typically store and pass them as values, not pointers. That is, time variables and struct fields should be of type time.Time, not *time.Time.

第二种方式

```
// Scan implements the Scanner interface.
func (nt *NullTime) Scan(value interface{}) error {
nt.Time, nt.Valid = value.(time.Time)
return nil
}
// Value implements the driver Valuer interface.
func (nt NullTime) Value() (driver.Value, error) {
if !nt.Valid {
return nil, nil
}
return nt.Time, nil
}
func (nt *NullTime) MarshalJSON() ([]byte, error) {
if !nt.Valid {
return nil, nil
}
val := fmt.Sprintf("\"%s\"", nt.Time.Format(time.RFC3339))
return []byte(val), nil
}
type A struct {
Id int
UserId int
// VipExpireTime time.Time
// VipExpireTime *time.Time
VipExpireTime NullTime
MessageType string
// ClickTabTime time.Time
// ClickTabTime *time.Time
ClickTabTime NullTime
CreateTime time.Time `gorm:"default:current_time"`
UpdateTime time.Time `gorm:"default:current_time"`
}
type NullTime struct {
mysql.NullTime
}
```

可以参照这篇文章做处理:[How I handled possible null values from database rows in Golang?](https://medium.com/aubergine-solutions/how-i-handled-null-possible-values-from-database-rows-in-golang-521fb0ee267)

然后我们说一下MySQL中[explicit_defaults_for_timestamp](https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_explicit_defaults_for_timestamp)属性,这与timestamp的默认值类型与表现形式有关。

>注意explicit_defaults_for_timestamp本身已被弃用因为它的唯一目的是允许控制将来在MySQL版本中删除的已弃用的TIMESTAMP行为当删除这些行为时explicit_defaults_for_timestamp将没有任何用途也将被删除

查看`explicit_defaults_for_timestamp`当前的状态

```
SHOW VARIABLES LIKE 'explicit_defaults_for_timestamp';
explicit_defaults_for_timestamp: OFF
```

然后参考MySQL文档中给出的方式处理,[Automatic Initialization and Updating for TIMESTAMP and DATETIME](https://dev.mysql.com/doc/refman/5.6/en/timestamp-initialization.html)

因为`timestamp`会有 `create_time``update_time`这种字段如果不赋值gorm会按照零值处理所以可以在字段后加`tag`

```
type A struct {
Id int
UserId int
VipExpireTime time.Time
MessageType string
ClickTabTime time.Time
CreateTime time.Time `gorm:"default:current_time"`
UpdateTime time.Time `gorm:"default:current_time on update current_time"`
}
对应数据库字段:
CREATE TABLE `a` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`vip_expire_time` timestamp DEFAULT 0 COMMENT 'vip终止时间',
`message_type` varchar(50) NOT NULL DEFAULT '' COMMENT '消息的类型',
`click_tab_time` timestamp DEFAULT 0 COMMENT '点击Tab的时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
COMMENT='a表';
```

这样对于后续业务的时间判断就可以利用 time.IsZero() 来判断

看这个讨论[Should I use the datetime or timestamp data type in MySQL?](https://stackoverflow.com/questions/409286/should-i-use-the-datetime-or-timestamp-data-type-in-mysql)

当然直接把MySQL字段设置为datetime也可以规避此类问题但是对于使用datetime还是timestamp
个人还是倾向timestamp吧因为随时区变化空间效率更高

扩展

datetime timestamp

***datetime***

范围最大`1001``9999`时间格式为`YYYYMMDDHHMMSS`与时区无关使用**8个字节**存储

如果没有指定 `default`datetime 默认为 `null`
```
Go中time.Time{}为零时,值为:0001-01-01 00:00:00 +0000 UTC
```

***timestamp***

保存了从`1970年1月1日午夜`以来的秒数与UNIX时间戳相同范围从1970年到2038年使用**4个字节**存储显示的值依赖于时区

timestamp可以配置插入更新的行为
如果没有指定 `default`timestamp 默认为 `0`( 1970-01-01 00:00:00)。

如果强行更新小于1970年的值会报错
```
Incorrect datetime value: '1969-12-01 00:00:00' for column 'ts' at row 1
```

UTC/GMT/时间戳

1.UTC时间 GMT时间

我们可以认为格林威治时间就是时间协调时间GMT=UTC),格林威治时间和UTC时间均用秒数来计算的

2.UTC时间 本地时

UTC + 时区差本地时间
时区差东为正西为负在此把东八区时区差记为 +0800

UTC + (+0800) = 本地北京时间 (1)

那么UTC = 本地时间北京时间))- 0800 (2)

3.UTC Unix时间戳

在计算机中看到的UTC时间都是从1970年01月01日 0:00:00)开始计算秒数的所看到的UTC时间那就是从1970年这个时间点起到具体时间共有多少秒这个秒数就是Unix时间戳


参考资料

高性能MySQL

[Automatic Initialization and Updating for TIMESTAMP and DATETIME](https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html)

[How I handled possible null values from database rows in Golang?](https://medium.com/aubergine-solutions/how-i-handled-null-possible-values-from-database-rows-in-golang-521fb0ee267)

0 comments on commit f367942

Please sign in to comment.