跳到主要内容

使用 Ent 实现 Twitter 编辑按钮

· 阅读需 8 分钟

Twitter 的“编辑按钮”功能因埃隆·马斯克的投票推文而登上头条,询问用户是否想要此功能。

Elons Tweet

毫无疑问,这是 Twitter 最受用户期待的功能之一。

作为一名软件开发者,我立即开始思考如何自己实现这一功能。跟踪/审计问题在许多应用中非常常见。如果你有一个实体(例如 Tweet)并且想要跟踪其中某个字段(例如 content 字段)的变更,有许多常见的解决方案。一些数据库甚至提供专有的方案,如 Microsoft 的变更跟踪(Change Tracking)和 MariaDB 的系统版本表(System Versioned Tables)。然而,在大多数使用场景中,你需要自己“拼装”这一功能。幸运的是,Ent 提供了一个模块化的扩展系统,只需几行代码便可插入类似功能。

Twitter+Edit Button

如果只有……

Ent 简介

Ent 是一个为 Go 设计的实体框架,能够让开发大型应用变得轻而易举。Ent 预装了许多壮观的功能,例如:

凭借 Ent 的代码生成引擎和先进的扩展系统,你可以轻松地用高级功能对 Ent 客户端进行模块化,这些功能通常需要手动实现时会非常耗时。举例而言:

Enthistory

enthistory 是一个扩展,我们在想给我们的某个 web 服务添加“活动与历史”面板时开始开发。该面板的作用是显示谁在什么时候更改了什么(即审计)。在 Atlas,这个用于使用声明式 HCL 文件管理数据库的工具,我们有一个名为“schema”的实体,基本上是一个大文本块。任何对 schema 的更改都会被记录,并可随后在“活动与历史”面板中查看。

Activity and History

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" 的表,其中一列是推文内容。

idcontentcreated_atauthor_id
1Hello Twitter!2022-04-06T13:45:34+00:00123
2Hello Gophers!2022-04-06T14:03:54+00:00456

现在,假设我们想允许用户编辑内容,同时在前端实时显示变更。为了解决此问题,有几种常见方法,各有优缺点,本文先不深入讨论。一个可行的思路是创建一个 "tweets_history" 表来记录推文的变更:

idtweet_idtimestampeventcontent
112022-04-06T12:30:00+00:00CREATEDhello world!
222022-04-06T13:45:34+00:00UPDATEDhello 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 扩展集成,并为其底层实现提供更多方法。

有关更多 Ent 新闻与更新: