如何用sqlx将PostgreSQL的timestamp反序列化为google.protobuf.Timestamp?
我有一个PostgreSQL表,其中creation_time字段定义如下:
creation_time timestamp with time zone not null default now(),
同时定义了如下Protobuf消息:
message Thing { //...Other fields here google.protobuf.Timestamp creation_time = 1; }
通过protoc生成Go结构体后,得到如下结构:
type Thing struct { CreationTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty"` }
使用sqlx读写PostgreSQL数据库时,整数、字符串等常规字段反序列化正常,但CreationTime字段(类型为*timestamppb.Timestamp)无法被sqlx处理——因为该类型没有实现Value和Scan方法。
我不想采用“Protobuf结构体仅用于API通信,内部用普通Go结构体加适配器转换”的方案,也不想在多处代码中重复用sqlx.Scan单独处理时间戳,考虑过实现自定义的Timestamp Protobuf类型(包装Google的Timestamp)来实现Scanner和Valuer接口,想知道大家通常怎么处理这类场景。
1. 包装timestamppb.Timestamp实现数据库接口
这是最常用也最贴合需求的方案:自定义一个包装类型,实现database/sql标准库的Scanner和Valuer接口,这样sqlx就能自动处理该类型的序列化与反序列化,无需在业务代码中重复处理。
示例代码:
import ( "database/sql/driver" "fmt" "time" "google.golang.org/protobuf/types/known/timestamppb" ) type ProtoTimestamp struct { *timestamppb.Timestamp } // Scan 实现Scanner接口,将数据库读取的值转换为ProtoTimestamp func (pt *ProtoTimestamp) Scan(value interface{}) error { var t time.Time switch v := value.(type) { case time.Time: t = v case []byte: var err error t, err = time.Parse(time.RFC3339, string(v)) if err != nil { return err } case string: var err error t, err = time.Parse(time.RFC3339, v) if err != nil { return err } default: return fmt.Errorf("unsupported type for ProtoTimestamp: %T", value) } pt.Timestamp = timestamppb.New(t) return nil } // Value 实现Valuer接口,将ProtoTimestamp转换为数据库可接受的格式 func (pt ProtoTimestamp) Value() (driver.Value, error) { if pt.Timestamp == nil { return nil, nil } return pt.Timestamp.AsTime(), nil }
之后只需修改Protobuf定义(导入自定义类型的proto文件),或者直接将生成的Go结构体中CreationTime的类型替换为*ProtoTimestamp即可。
2. 注册sqlx全局类型映射
如果不想修改生成的结构体类型,可以通过sqlx.RegisterType为timestamppb.Timestamp绑定全局的转换逻辑,只需在程序启动时注册一次,所有相关字段都会自动适配:
import ( "database/sql/driver" "fmt" "time" "github.com/jmoiron/sqlx" "google.golang.org/protobuf/types/known/timestamppb" ) func init() { err := sqlx.RegisterType("timestamptz", (*timestamppb.Timestamp)(nil), // 写入数据库时的转换逻辑 func(val interface{}) (driver.Value, error) { ts, ok := val.(*timestamppb.Timestamp) if !ok { return nil, fmt.Errorf("expected *timestamppb.Timestamp, got %T", val) } return ts.AsTime(), nil }, // 从数据库读取时的转换逻辑 func(dest interface{}, src driver.Value) error { tsPtr, ok := dest.(**timestamppb.Timestamp) if !ok { return fmt.Errorf("expected **timestamppb.Timestamp, got %T", dest) } t, ok := src.(time.Time) if !ok { return fmt.Errorf("expected time.Time, got %T", src) } *tsPtr = timestamppb.New(t) return nil }, ) if err != nil { panic(fmt.Sprintf("failed to register sqlx type: %v", err)) } }
3. 自定义protoc插件批量处理
如果项目中有大量Protobuf Timestamp字段需要适配数据库,可以编写一个自定义protoc插件,在生成Go代码时自动为google.protobuf.Timestamp类型添加Scanner和Valuer接口实现。这种方式适合大型项目,一次性解决所有Timestamp字段的兼容问题,但需要一定的插件开发成本。
内容的提问来源于stack exchange,提问作者JPFrancoia

