最近找了实习开始上班,公司项目使用的 ORM 框架是 ent 而不是我熟悉的 gorm,因此在使用过程中踩了不少坑,这里记录其中之一。

ent

ent 是 Facebook 开源的一个简单而又功能强大的 Go 语言实体框架,主要具备以下几个特性:

  • 图就是代码 - 将任何数据库表建模为 Go 对象
  • 轻松地遍历任何图形 - 可以轻松地运行查询、聚合和遍历任何图形结构
  • 静态类型和显式API - 使用代码生成静态类型和显式 API,查询数据更加便捷
  • 多存储驱动程序 - 支持 MySQL, PostgreSQL, SQLite 和 Gremlin
  • 可扩展 - 简单地扩展和使用 Go 模板自定义

entgorm 的区别主要在于:

  • ent 通过代码生成创建类型安全的 API,避免运行时反射,性能更优
  • gorm 依赖运行时反射动态解析结构体,灵活但牺牲部分性能和类型安全

二者在使用体验上的差别还是挺大的,可以根据喜好自行选择。
另外 ent 实际上和基于 gorm 的安全 ORM 框架 Gen 很类似,都是通过代码生成的方式类型安全的 CRUD 代码。

问题现象

在开发完功能进行测试时,发现表里每条记录的创建时间(create_time)都一样。

这让我一下子有些摸不着头脑,在网上查了半天也没找到有用解答,只能回去扒拉官方文档,最后给我找到了不同。
这边给出一个对比。

  • 我代码中 create_time 字段的默认值设置

  • 官网文档的默认值设置示例

相信眼尖的已经发现区别了,就是 Default 设置的 time.Now()time.Now,一()之差,谬之千里。

原因解析

time.Now() 相信 Go 的开发者都不会陌生(望文生义也能看懂),它是 Go 标准库中用于获取当前时间的函数,但是 time.Now 或许就有点眼生了,因为平常很少会这样写,不过给一个例子大概就能理解了。

在 Go 中,函数是一等公民,我们可以将函数作为参数传递,time.Now 就是一个函数,它的类型是 func() time.Time,也就是说它是一个无参数的函数,返回值是 time.Time 类型。
有了上面的基础,我们就不难理解 Default(time.Now)Default(time.Now()) 的区别了。

  • Default(time.Now())
    • ​在程序启动时立即执行,即将启动时间作为固定默认值,导致所有新记录在给 create_time 赋值时都会使用该值,造成本文创建时间的异常问题
  • Default(time.Now)
    • time.Now​传递的是函数指针ent 框架会在每次插入新记录动态调用该函数,即达到设置创建时间的效果

根据 Default 的函数注释,可以发现其期望是设置一个用于生成默认时间的函数但凡看一眼 For example)。

另外也可以从 Git 来看一下修改前后 ent 生成代码的 diff ,可以发现本质上就是值和函数的区别。



举一反三

在上述基础上,假如我们有一个需求是设置借书的应还时间字段默认值为借书时间(即当前时间)的 7 天后,我们自然可以想到这样写:

1
2
3
4
5
6
7
8
9
func DueTime() time.Time {
return time.Now().AddDate(0, 0, 7)
}

// ...

field.Time("due_time").SchemaType(map[string]string{
dialect.MySQL: "datetime",
}).Default(DueTime).Comment("借书应还时间")

不过具体如何实现还是要看具体需求,这里只是举个栗子。

后记

对于用惯了 gorm 的我来说 ent 还是比较新的开发体验,目前个人日常开发已转向使用 gorm 下同类型的 Gen,推荐尝试。