Twitter 的“编辑按钮”功能因埃隆·马斯克的投票推文而登上头条,询问用户是否想要此功能。
毫无疑问,这是 Twitter 最受用户期待的功能之一。
作为一名软件开发者,我立即开始思考如何自己实现这一功能。跟踪/审计问题在许多应用中非常常见。如果你有一个实体(例如 Tweet)并且想要跟踪其中某个字段(例如 content 字段)的变更,有许多常见的解决方案。一些数据库甚至提供专有的方案,如 Microsoft 的变更跟踪(Change Tracking)和 MariaDB 的系统版本表(System Versioned Tables)。然而,在大多数使用场景中,你需要自己“拼装”这一功能。幸运的是,Ent 提供了一个模块化的扩展系统,只需几行代码便可插入类似功能。

如果只有……
Ent 简介
Ent 是一个为 Go 设计的实体框架,能够让开发大型应用变得轻而易举。Ent 预装了许多壮观的功能,例如:
- 类型安全的生成 CRUD API(CRUD API)
- 复杂的图遍历(Graph traversals)(SQL 连接变得简单)
- 分页(Paging)
- 隐私(Privacy)
- 安全的数据库迁移(Safe DB migrations)
凭借 Ent 的代码生成引擎和先进的扩展系统,你可以轻松地用高级功能对 Ent 客户端进行模块化,这些功能通常需要手动实现时会非常耗时。举例而言:
Enthistory
enthistory 是一个扩展,我们在想给我们的某个 web 服务添加“活动与历史”面板时开始开发。该面板的作用是显示谁在什么时候更改了什么(即审计)。在 Atlas,这个用于使用声明式 HCL 文件管理数据库的工具,我们有一个名为“schema”的实体,基本上是一个大文本块。任何对 schema 的更改都会被记录,并可随后在“活动与历史”面板中查看。

Atlas 的“活动与历史”屏幕
此功能在许多应用中很常见,例如 Google 文档、GitHub PR、Facebook 帖子等,但在极受欢迎的 Twitter 中却缺失。
超过 300 万人投票支持在 Twitter 上添加“编辑按钮”,接下来让我展示 Twitter 如何毫不费力地让用户满意!
使用 Enthistory,你只需像下面这样注解你的 Ent 模式:
func (Tweet) Fields() []ent.Field {
return []ent.Field{
field.String("content").
Annotations(enthistory.TrackField()),
field.Time("created").
Default(time.Now),
}
}
Enthistory 会挂钩到你的 Ent 客户端,确保对 "Tweet" 的每一次 CRUD 操作都被记录到 "tweets_history" 表中,无需修改代码,并为你提供 API 来消费这些记录:
// 创建新 Tweet 时不会影响已存在的历史,enthistory 将自动在运行时修改事务以记录此事件到历史表
client.Tweet.Create().SetContent("hello world!").SaveX(ctx)
// 查询历史变更就像查询任何其它实体的一条边一样简单
t, _ := client.Tweet.Get(ctx, id)
hs := client.Tweet.QueryHistory(t).WithChanges().AllX(ctx)
若没有使用 Enthistory,你需要做的事情会更繁琐:假设你有一个类似 Twitter 的应用。它有一个名为 "tweets" 的表,其中一列是推文内容。
| id | content | created_at | author_id |
|---|---|---|---|
| 1 | Hello Twitter! | 2022-04-06T13:45:34+00:00 | 123 |
| 2 | Hello Gophers! | 2022-04-06T14:03:54+00:00 | 456 |
现在,假设我们想允许用户编辑内容,同时在前端实时显示变更。为了解决此问题,有几种常见方法,各有优缺点,本文先不深入讨论。一个可行的思路是创建一个 "tweets_history" 表来记录推文的变更:
| id | tweet_id | timestamp | event | content |
|---|---|---|---|---|
| 1 | 1 | 2022-04-06T12:30:00+00:00 | CREATED | hello world! |
| 2 | 2 | 2022-04-06T13:45:34+00:00 | UPDATED | hello Twitter! |
通过类似上述的表,我们可以记录原始推文「1」的变更,并在需要时展示它最初在 12:30:00 的内容为 "hello world!",随后在 13:45:34 被修改为 "hello Twitter!"。
实现这一点时,我们需要在对 "tweets" 的每一次 UPDATE 语句中加入一次 INSERT 到 "tweets_history"。为了正确性,必须将两条语句包裹在一个事务中,以避免第一条成功后第二条失败导致历史记录被破坏。我们还需要确保每一次 INSERT 到 "tweets" 都与一次 INSERT 到 "tweets_history" 配合:
# INSERT 被记录为 "CREATE" 历史事件
- INSERT INTO tweets (`content`) VALUES ('Hello World!');
+BEGIN;
+INSERT INTO tweets (`content`) VALUES ('Hello World!');
+INSERT INTO tweets_history (`content`, `timestamp`, `record_id`, `event`)
+VALUES ('Hello World!', NOW(), 1, 'CREATE');
+COMMIT;
# UPDATE 被记录为 "UPDATE" 历史事件
- UPDATE tweets SET `content` = 'Hello World!' WHERE id = 1;
+BEGIN;
+UPDATE tweets SET `content` = 'Hello World!' WHERE id = 1;
+INSERT INTO tweets_history (`content`, `timestamp`, `record_id`, `event`)
+VALUES ('Hello World!', NOW(), 1, 'UPDATE');
+COMMIT;
这个方法虽好,但若你还需要对其他实体(如 "comment_history"、"settings_history")进行同样处理,就会需要创建更多表。为了避免这种情况,Enthistory 统一使用单一的 "history" 表和单一的 "changes" 表来记录所有跟踪字段。它还能支持多种字段类型而无需额外添加列。
预发布
Enthistory 仍处于早期设计阶段,正在内部测试。因此,我们尚未将其公开发布,尽管我们计划很快发布。若你想尝试 Enthistory 的预发布版本,我写了一个简单的 React 应用,配合 GraphQL+Enthistory 展示推文编辑效果。你可以在这里查看:here。欢迎随时提供反馈。
小结
我们已经看到 Ent 的模块化扩展系统如何让你像安装一个包一样快速实现高级功能。开发自己的扩展既有趣、简单又能增长见识(link)!欢迎大家亲自试一试!
未来,Enthistory 将用于跟踪边(即外键表)、与 OpenAPI 与 GraphQL 扩展集成,并为其底层实现提供更多方法。
- 订阅我们的【简报】(https://entgo.substack.com/)
- 在【Twitter】(https://twitter.com/entgo_io)上关注我们
- 在 Gophers Slack 的 #ent 频道加入我们 (https://entgo.io/docs/slack)
- 加入我们的 Ent Discord 服务器 (https://discord.gg/qZmPgTE6RX)
