如何让GORM在PostgreSQL中使用数据库端now()函数填充created_at、updated_at、deleted_at字段
now()替代客户端时间 针对你遇到的多应用实例时间不同步、需要精确追踪数据变更时间的问题,最优雅的解决方案是替换GORM的默认时间回调函数,让它自动使用PostgreSQL的now()来生成created_at、updated_at和deleted_at字段的值——不需要额外的原生SQL或单独事务,完全兼容你现有的业务代码。
核心思路
GORM默认会用客户端本地时间填充时间字段,我们只需要替换它内置的create_time、update_time、delete_time回调,让这些回调生成调用数据库now()的表达式即可。
完整实现代码
以下是修改后的代码,直接替换你原有的代码即可生效:
package main import ( "log" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/logger" ) type Data struct { gorm.Model Value string } func init() { // 替换Create操作的时间回调:用now()设置created_at和updated_at gorm.DefaultCallback.Create().Replace("gorm:create_time", func(db *gorm.DB) { if !db.Statement.Schema.ModelStruct.PrioritizedPrimaryField.NotNull { db.Statement.SetColumn("created_at", gorm.Expr("now()")) db.Statement.SetColumn("updated_at", gorm.Expr("now()")) } }) // 替换Update操作的时间回调:用now()更新updated_at gorm.DefaultCallback.Update().Replace("gorm:update_time", func(db *gorm.DB) { if db.Statement.Changed() { db.Statement.SetColumn("updated_at", gorm.Expr("now()")) } }) // 替换软删除操作的时间回调:用now()设置deleted_at gorm.DefaultCallback.Delete().Replace("gorm:delete_time", func(db *gorm.DB) { if db.Statement.Schema != nil && db.Statement.Schema.HasSoftDelete { db.Statement.SetColumn("deleted_at", gorm.Expr("now()")) db.Statement.AddClause(clause.Set{ Column: clause.Column{Name: "deleted_at"}, Value: gorm.Expr("now()"), }) db.Statement.AddClause(clause.Where{ Exprs: []clause.Expression{clause.Neq{ Column: clause.Column{Name: "deleted_at"}, Value: gorm.Expr("now()"), }}, }) db.Statement.SQL.WriteString("UPDATE ") db.Statement.Build(clause.Update{}) } }) } func main() { // 初始化测试数据库可执行Docker命令: // $ docker run --rm -e POSTGRES_HOST_AUTH_METHOD=trust --name pg -p 5432:5432 postgres db, err := dbClient() errHandle(err) err = db.AutoMigrate(&Data{}) errHandle(err) data := &Data{Value: "some value"} // 新增数据 err = db.Create(&data).Error errHandle(err) data.Value = "some changes" // 更新数据 err = db.Omit("created_at").Save(&data).Error errHandle(err) // 软删除 err = db.Delete(&data).Error errHandle(err) } func errHandle(err error) { if err != nil { log.Fatal(err) } } func dbClient() (*gorm.DB, error) { newLogger := logger.Default.LogMode(logger.Info) c := &gorm.Config{ Logger: newLogger, } d := postgres.Open("dbname=postgres host=localhost user=postgres") return gorm.Open(d, c) }
效果验证
修改后,执行代码生成的SQL会完全符合你的期望:
INSERT INTO "data" ("created_at","updated_at","deleted_at","value") VALUES (now(),now(),NULL,'some value') RETURNING "id";
UPDATE "data" SET "updated_at"=now(),"deleted_at"=NULL,"value"='some changes' WHERE "id" = 1;
UPDATE "data" SET "deleted_at"=now() WHERE "data"."id" = 1 AND "data"."deleted_at" IS NULL;
备选方案:手动指定字段值(适合局部场景)
如果你只需要在特定操作中使用数据库时间,可以手动用gorm.Expr("now()")指定字段值,比如:
// 新增数据时手动指定时间 err = db.Model(&Data{}).Create(map[string]interface{}{ "Value": "some value", "created_at": gorm.Expr("now()"), "updated_at": gorm.Expr("now()"), }).Error // 更新数据时手动更新updated_at err = db.Model(&data).Updates(map[string]interface{}{ "Value": "some changes", "updated_at": gorm.Expr("now()"), }).Error
但这种方法需要每次操作都手动处理,不如替换回调的方式一劳永逸。
注意事项
PostgreSQL的now()函数返回的是事务开始的时间,这意味着同一个事务内的所有操作会使用同一个时间戳,非常适合需要精确追踪数据变更时序的场景,彻底避免了客户端时间不一致的问题。
内容的提问来源于stack exchange,提问作者user3805530

