在 GO 中使用哪种 ORM:GORM、sqlc、Ent 还是 Bun?
GORM 与 sqlc 与 Ent 与 Bun
Go 的生态系统提供了多种 ORM(对象关系映射)工具和数据库库,每种工具都有其独特的理念。以下是对四种主要解决方案的全面比较:在 Go 中使用 PostgreSQL 的 GORM、sqlc、Ent 和 Bun。
让我们从性能、开发人员体验、流行度和功能/可扩展性等方面对它们进行评估,并通过代码示例演示对 User
模型的基本 CRUD 操作。中级和高级 Go 开发人员将了解每种工具的权衡,并找出最适合他们需求的方案。
ORM 概述
-
GORM – 一个功能丰富的、Active Record 风格的 ORM。GORM 允许你将 Go 结构体定义为模型,并提供一个广泛的 API,通过方法链来查询和操作数据。GORM 已经使用多年,是使用最广泛的 Go ORM 之一(GitHub 上有近 39k 星标)。GORM 的吸引力在于其强大的功能集(自动迁移、关系处理、急加载、事务、钩子等),这使得可以使用最少的原始 SQL 编写干净的 Go 代码。然而,它引入了自己的模式,并由于反射和接口的使用导致运行时开销,这可能会影响大规模操作的性能。
-
sqlc – 不是一个传统的 ORM,而是一个 SQL 代码生成器。使用 sqlc,你编写普通的 SQL 查询(在
.sql
文件中),然后该工具生成类型安全的 Go 代码(DAO 函数)来执行这些查询。这意味着你通过调用生成的 Go 函数(例如CreateUser
、GetUser
)与数据库进行交互,并获得强类型的返回结果,而无需手动扫描行。sqlc 实际上允许你使用带有编译时安全性的原始 SQL —— 没有查询构建的中间层或运行时反射。它通过利用预处理 SQL 完全避免了运行时开销,使其速度与直接使用database/sql
一样快。权衡是,你必须编写(并维护)查询的 SQL 语句,与自动构建 SQL 的 ORM 相比,动态查询逻辑可能不太直接。 -
Ent(Entgo) – 一个 代码优先的 ORM,使用代码生成来创建数据模型的 类型安全 API。你使用 Go(通过 Ent 的 DSL 定义字段、边/关系、约束等)定义你的模式,然后 Ent 会为模型和查询方法生成 Go 包。生成的代码使用流畅的 API 来构建查询,并具有完整的编译时类型检查。Ent 相对较新(由 Linux 基金会支持,由 Ariga 开发)。它专注于 开发人员体验和正确性 —— 查询是通过链式方法构建的,而不是通过原始字符串,这使得它们更容易组合且更少出错。Ent 的方法会产生高度优化的 SQL 查询,甚至会缓存查询结果以减少重复访问数据库的次数。它被设计为可扩展,因此可以高效处理复杂的模式和关系。然而,使用 Ent 会增加一个构建步骤(运行代码生成),并将其绑定到其生态系统。目前,Ent 支持 PostgreSQL、MySQL、SQLite(以及对其他数据库的实验性支持),而 GORM 支持更广泛的数据库(包括 SQL Server 和 ClickHouse)。
-
Bun – 一个较新的 ORM,采用 “SQL 优先” 的方法。Bun 受 Go 的
database/sql
和较早的 go-pg 库启发,旨在 轻量且快速,并专注于 PostgreSQL 的功能。Bun 不使用 Active Record 模式,而是提供一个 流畅的查询构建器:你从db.NewSelect()
/NewInsert()
等开始查询,并使用与 SQL 非常相似的方法构建查询(你甚至可以嵌入原始 SQL 片段)。它使用与 GORM 类似的结构标签将查询结果映射到 Go 结构体。Bun 倾向于显式性,并在需要时允许轻松回退到原始 SQL。Bun 不会自动运行迁移(你需要编写迁移或手动使用 Bun 的迁移包),这被一些人视为控制的加分项。Bun 因其 优雅处理复杂查询 和对高级 Postgres 类型(数组、JSON 等)的原生支持而受到称赞。在实践中,Bun 对 GORM 的运行时开销要小得多(没有每个查询的重反射),这在高负载场景中意味着更好的吞吐量。其社区规模较小(约 4k 星标),其功能集虽然坚实,但不如 GORM 广泛。Bun 可能需要一些 SQL 知识才能有效使用,但它以性能和灵活性作为回报。
性能基准
在性能(查询延迟和吞吐量)方面,这些工具的行为有显著差异,尤其是在负载增加时。最近的基准测试和用户经验突出了几个关键点:
-
GORM: GORM 的便利性是以牺牲一些性能为代价的。对于简单操作或小结果集,GORM 可以非常快 —— 事实上,一个基准测试显示 GORM 在非常小的查询(例如获取 1 或 10 条记录)中执行时间最快。然而,随着记录数量的增加,GORM 的开销会导致其 显著落后。在测试中获取 15,000 条记录时,GORM 大约是 sqlc 或甚至原始
database/sql
的两倍慢(GORM 为 59.3 毫秒,而 sqlc 和 database/sql 分别为约 31.7 毫秒和 32.0 毫秒)。反射密集型设计和查询构建抽象会增加延迟,GORM 可能会为复杂负载发出多个查询(例如使用Preload
默认情况下每个相关实体发出一个查询),这会放大延迟。总结: GORM 通常在中等负载下表现良好,但在高吞吐量场景或大规模批量操作中,其开销可能会成为瓶颈。 -
sqlc: 因为 sqlc 的调用本质上是手写的 SQL,其性能与直接使用标准库相当。基准测试一致地将 sqlc 放在最快选项之一,通常仅比原始
database/sql
稍慢或甚至稍快。例如,在 10k+ 记录时,sqlc 被观察到在吞吐量上略微领先于原始database/sql
(可能是因为避免了一些扫描样板代码并使用了高效的代码生成)。使用 sqlc 时,没有运行时查询构建器 —— 你的查询以预处理语句的形式执行,开销极小。关键是,你已经完成了编写一个优化 SQL 查询的工作;sqlc 只是省去了将行扫描到 Go 类型中的麻烦。实际上,这意味着 出色的可扩展性 —— sqlc 可以像原始 SQL 一样处理大量数据负载,使其在需要原始速度时成为首选。 -
Ent: Ent 的性能介于原始 SQL 方法和传统 ORM 之间。因为 Ent 生成类型安全代码,它避免了反射和大多数运行时查询构建成本。它生成的 SQL 非常优化(你可以通过 Ent API 控制编写高效的查询),并且 Ent 包含一个内部缓存层,可以在某些情况下重用查询执行计划/结果。这可以减少复杂工作流中重复的数据库访问。虽然 Ent 与其他工具的具体基准测试结果各不相同,但许多开发者报告称 Ent 在等效操作中优于 GORM,尤其是在复杂性增加时。一个原因是 GORM 可能会生成次优查询(例如,如果没有仔细使用连接/预加载,可能会出现 N+1 选择),而 Ent 鼓励显式的急加载关系,并会在更少的查询中连接数据。Ent 每个操作通常分配的内存也比 GORM 少,这可以通过减少对 Go 垃圾收集器的压力来提高吞吐量。总体而言,Ent 是为高性能和大型模式构建的 —— 其开销较低,可以高效处理复杂查询 —— 但在某些情况下,它可能无法匹配手写 SQL(sqlc)的原始吞吐量。如果希望同时拥有速度和 ORM 层的安全性,Ent 是一个强有力的竞争对手。
-
Bun: Bun 是为了性能而创建的,通常被引用为比 GORM 更快的替代方案。它使用流畅的 API 来构建 SQL 查询,但这些构建器是轻量级的。Bun 不会隐藏 SQL —— 它更像是
database/sql
的一层薄薄的覆盖,这意味着你只会承受 Go 标准库所施加的极少开销。用户在大型项目中从 GORM 切换到 Bun 时注意到显著的改进:例如,一份报告提到 GORM 在他们的规模下“超级慢”,而用 Bun(和一些原始 SQL)解决了他们的性能问题。在 go-orm-benchmarks 等基准测试中,Bun 通常在大多数操作中接近顶部(通常在原始 sqlx/sql 的 1.5 倍以内),并且在吞吐量上远超 GORM。它还支持 批量插入、手动控制连接与单独查询,以及其他开发者可以利用的优化。总结: Bun 提供接近裸机的性能,并且是当你想要 ORM 便利性但不能牺牲速度时的绝佳选择。其 SQL 优先的性质确保你始终可以优化查询,如果生成的查询不够优化。
性能总结: 如果目标是最大性能(例如数据密集型应用、高负载下的微服务),sqlc 或甚至手写的 database/sql
调用是赢家。它们几乎没有抽象成本,并且可以线性扩展。Ent 和 Bun 也表现非常好,可以高效处理复杂查询 —— 它们在速度和抽象之间取得平衡。GORM 提供了最多的功能,但以开销为代价;它对许多应用来说是完全可接受的,但如果你预计要处理大量数据或需要超低延迟,你应留意其影响(在这种情况下,你可以通过谨慎使用 Joins
而不是 Preload
来减少查询数量,或在关键路径中混合使用原始 SQL 来缓解 GORM 的成本,但这会增加复杂性)。
开发者体验和易用性
开发者体验可能是主观的,但它涵盖了学习曲线、API 的清晰度以及日常编码中的生产力。以下是我们的四个候选者在易用性和开发工作流程方面的比较:
-
GORM – 功能丰富但有学习曲线: GORM 通常是 Go 新手尝试的第一个 ORM,因为它承诺全自动体验(你定义结构体,可以立即 Create/Find 而无需编写 SQL)。文档非常全面,有很多指南,这有助于学习。然而,GORM 有其自己的惯用方式(“基于代码的数据库交互方式”),如果你习惯于原始 SQL,它一开始可能会 感觉有些笨拙。许多常见操作都很直接(例如
db.Find(&objs)
),但当你进入关联、多态关系或高级查询时,你需要学习 GORM 的惯例(结构体标签、Preload
vsJoins
等)。这就是为什么经常提到 陡峭的初始学习曲线。一旦你克服了这个曲线,GORM 可以非常高效 —— 你花更少的时间编写重复的 SQL,更多的时间在 Go 逻辑上。事实上,掌握后,许多人发现他们用 GORM 开发功能的速度比用低级库更快。简而言之,GORM 的 DX(开发者体验):初始复杂度高,但随着经验增加会变得顺畅。庞大的社区意味着有大量示例和 StackOverflow 答案可用,丰富的插件生态系统可以进一步简化任务(例如,用于软删除、审计等的插件)。主要的注意事项是你必须始终意识到 GORM 在幕后做了什么,以避免陷阱(如意外的 N+1 查询)。但对愿意投入时间学习 GORM 的开发者来说,它确实“感觉”像一个强大、集成的解决方案,为你处理了很多事情。 -
Ent – 模式优先且类型安全: Ent 明确设计用于改善 ORM 的开发者体验。你在一个地方描述你的模式(Go 代码),并获得一个强类型的 API 来使用 —— 这意味着 没有字符串类型的查询,许多错误在编译时就被捕获。例如,如果你尝试引用一个不存在的字段或边,你的代码将无法编译。这种安全网是大型项目中的巨大 DX 优势。Ent 的查询构建 API 是 流畅且直观的:你可以链式调用方法,如
.Where(user.EmailEQ("alice@example.com"))
,它几乎像英语一样读起来。来自其他语言 ORM 的开发者通常发现 Ent 的方法自然(它有点像 Entity Framework 或 Prisma,但用 Go 实现)。学习 Ent 需要理解其代码生成工作流程。每次你修改模式时,你需要运行entc generate
(或使用go generate
)。这是一个额外的步骤,但通常是构建过程的一部分。Ent 的学习曲线是中等的 —— 如果你了解 Go 和一些 SQL 基础知识,Ent 的概念(字段、边等)是直观的。事实上,许多人发现 Ent 比 GORM 更容易推理,因为你不需要猜测生成的 SQL 是什么;你可以从方法名预测它,如果需要,还可以输出查询用于调试。Ent 的文档和示例非常好,由公司支持确保其积极维护。总体 DX(开发者体验)与 Ent: 对于希望 清晰和安全 的开发者来说非常友好。你编写的代码比原始 SQL 更冗长,但它是自文档化的。此外,重构更安全(在模式中重命名字段并重新生成会更新所有查询中的使用)。缺点可能是你受到 Ent 代码生成提供的限制 —— 如果你需要非常定制的查询,你可能需要用 Ent API 编写(它可以处理复杂连接,但偶尔你可能会发现用原始 SQL 更容易)。Ent 在必要时允许原始 SQL 片段,但如果你发现自己经常这样做,你可能会质疑 ORM 是否是正确的选择。总结来说,Ent 为大多数 CRUD 和查询逻辑提供了 平滑的开发者体验,只要你能接受运行代码生成步骤并遵循 Ent 强制的模式。 -
Bun – 更接近 SQL,更少的魔法: 使用 Bun 与使用 GORM 或 Ent 的感觉不同。Bun 的哲学是 不隐藏 SQL —— 如果你了解 SQL,你已经知道如何在很大程度上使用 Bun。例如,要查询用户,你可能会写:
db.NewSelect().Model(&users).Where("name = ?", name).Scan(ctx)
。这是一个流畅的 API,但非常接近实际 SELECT 语句的结构。Bun 的学习曲线 通常较低 如果你 熟悉 SQL 本身。需要学习的“ORM 魔法”更少;你主要学习构建查询的方法名和结构标签惯例。这使得 Bun 对于使用过database/sql
或其他查询构建器(如sqlx
)的资深开发者来说非常易于上手。相比之下,一个不自信编写 SQL 的新手可能会发现 Bun 比 GORM 稍微不那么有帮助 —— Bun 不会自动通过关系为你生成复杂查询,除非你明确指定它们。然而,Bun 确实 支持关系和急加载(使用Relation()
方法连接表) —— 它只是显式地这样做,一些开发者更喜欢这种清晰度。Bun 的开发者体验 可以描述为 轻量且可预测。你为某些操作编写稍微多一点的代码(因为 Bun 通常要求你显式指定列或连接),但作为回报,你拥有更多的控制和可见性。内部魔法极少,因此调试更容易(你通常可以记录 Bun 构建的查询字符串)。Bun 的文档(uptrace.dev 指南)详尽,并包括迁移、事务等模式,尽管社区较小意味着第三方教程较少。另一个 DX 方面是可用的工具:Bun 作为database/sql
的扩展,意味着任何与sql.DB
兼容的工具(如调试代理、查询日志器)都可以轻松与 Bun 一起使用。总结: Bun 提供了 无花哨的体验:它感觉像是在 Go 中编写结构化的 SQL。这对于重视控制和性能的开发者来说是绝佳的,但可能不会像 GORM 那样对经验较少的开发者提供太多手把手的帮助。共识是,Bun 使 简单的事情变得容易,复杂的事情变得可能(就像原始 SQL 一样),而不会对你施加太多框架。 -
sqlc – 编写 SQL,获取 Go 代码: sqlc 的方法颠覆了通常的 ORM 叙述。与其编写 Go 代码来生成 SQL,你编写 SQL 并获取 Go 代码。对于热爱 SQL 的开发者来说,这非常棒 —— 你可以使用 SQL 的全部力量(复杂连接、CTE、窗口函数等)没有任何 ORM 限制。sqlc 本身的学习曲线非常小。如果你知道如何编写 SELECT/INSERT/UPDATE 的 SQL,你已经完成了最难的部分。你需要学习如何用
-- name: Name :one/many/exec
注释标注查询并设置配置文件,但这 微不足道。生成的代码将是简单的函数定义,你像任何 Go 函数一样调用它们。开发者体验的优点: 没有 ORM API 要学习,没有惊喜 —— 查询按原样运行。你避免了整个类别的 ORM 问题(例如,弄清楚为什么 ORM 生成了某个 JOIN 或如何调整查询)。此外,你的代码审查可以包括审查 SQL 本身,这通常对复杂逻辑更清晰。另一个大的 DX 优势是 类型安全和 IDE 支持 —— 生成的方法和结构体可以在你的编辑器中跳转,重构工具可以作用于它们,而原始字符串查询对 IDE 是不透明的。缺点是 sqlc 要求你管理 SQL 脚本。这意味着如果你的模式或需求发生变化,你需要手动更新或添加相关的 SQL 并重新运行代码生成。这并不困难,但比仅仅调用一个 ORM 方法更手动。此外,动态查询(SQL 的某些部分是条件的)可能很繁琐 —— 你要么编写多个 SQL 变体,要么使用 SQL 语法技巧。一些开发者提到这是 sqlc 方法的限制。实际上,你可以经常结构化你的数据访问,使得你不需要过于动态的 SQL,或者在边缘情况下调用原始database/sql
。但这是一个需要考虑的问题:sqlc 对于明确定义的查询非常出色,对于即兴查询构建则不太适合。总结: 对于精通 SQL 的开发者,使用 sqlc 感觉 自然且高度高效。几乎没有新东西要学习,它消除了重复的 Go 模板代码。对于不太熟悉 SQL 的开发者,sqlc 可能初期使用起来较慢(与自动为基本 CRUD 生成查询的 ORM 相比)。然而,许多 Go 开发者认为 sqlc 是必备的,因为它击中了一个甜蜜点:手动控制与高安全性,且没有运行时成本。
流行度与生态系统支持
采用和社区支持可能会影响你的选择——一个流行的库意味着更多的社区贡献、更好的维护和更多的学习资源。
-
GORM: 作为四个中最古老且最成熟的库,GORM拥有迄今为止最大的用户群和生态系统。目前它是GitHub上星级最高的Go ORM(超过38,000颗星),并被无数生产项目使用。维护者非常活跃,项目定期更新(GORM v2是一个重大改进,提升了性能和架构)。GORM流行的一个巨大优势是丰富的扩展和集成。有官方和第三方插件,例如数据库驱动(如PostgreSQL、MySQL、SQLite、SQL Server、ClickHouse)等,开箱即用。GORM还支持广泛的应用场景:迁移、模式自动生成、软删除、JSON字段(通过
gorm.io/datatypes
)、全文搜索等,通常通过内置功能或附加组件实现。社区还开发了各种工具,如gormt
(从现有数据库生成结构定义),以及许多教程和示例。如果你遇到问题,快速搜索很可能找到其他人提出的问题或Stack Overflow上的问题。生态系统总结: GORM得到了极好的支持。它是许多人的“默认”ORM选择,这意味着你会在框架和模板中找到它。另一方面,它的庞大可能会让人感觉有些笨重,但对许多人来说,这种权衡是值得的,因为社区的深度很高。 -
Ent: 尽管Ent是较新的(2019年左右开源),但它已经建立了强大的社区。拥有约16,000颗星,并且得到了Linux基金会的支持,它不是一个边缘项目。大型公司已经开始使用Ent,因为它具有以模式为中心的优势,而维护者(在Ariga)提供企业支持,这是对业务关键用途的信心。围绕Ent的生态系统正在增长:有Ent扩展(ent/go),用于OpenAPI/GraphQL集成、gRPC集成,甚至还有一个与Ent模式一起工作的SQL迁移工具。由于Ent生成代码,一些生态系统模式有所不同——例如,如果你想与GraphQL集成,你可能会使用Ent的代码生成来生成GraphQL解析器。Ent的学习资源很好(官方文档和一个涵盖简单应用程序的示例项目)。社区论坛和GitHub讨论非常活跃,有关模式设计的问题和技巧。就社区支持而言,Ent已经过去“早期采用者”阶段,被认为是生产就绪和可靠的。它可能还没有GORM那么多的Stack Overflow答案,但正在迅速赶上。需要注意的一点是:由于Ent稍微有些意见化(例如,它希望管理模式),你可能会更倾向于单独使用它,而不是与其他ORM一起使用。它不像GORM那样有“插件”,但你可以编写自定义模板来扩展代码生成或挂钩到生命周期事件(Ent为生成的客户端提供了钩子/中间件支持)。基金会的支持表明有长期支持,因此如果其模型符合你的需求,选择Ent是一个安全的选择。
-
Bun: Bun(属于Uptrace开源套件的一部分)正在获得关注,特别是在那些曾经是现在不再维护的
go-pg
库的粉丝中。拥有约4,300颗星,它是此次比较中社区最小的,但它是一个非常活跃的项目。维护者响应迅速,并且正在快速添加功能。Bun的社区对它的性能非常热情。你可以在Go论坛和Reddit上找到开发者讨论他们因为速度而转向Bun。然而,由于用户群较小,你可能不会总是立即找到针对小众问题的答案——有时你需要阅读文档/源代码或在Bun的GitHub或Discord(Uptrace提供社区聊天)中提问。扩展生态系统的范围更有限:Bun自带自己的迁移库、一个固定数据加载器,并与Uptrace的可观测性工具集成,但你不会找到GORM那样的大量插件。尽管如此,Bun与sql.DB
的使用兼容,因此你可以混合使用——例如,使用github.com/jackc/pgx
作为底层驱动,或与其他期望*sql.DB
的包集成。Bun不会把你锁住。支持方面,由于较新,文档是最新且示例是现代的(通常展示使用context等)。官方文档直接将Bun与GORM和Ent进行比较,这很有帮助。如果社区规模是一个问题,一个策略可能是采用Bun以利用其优势,但保持使用相对浅层(例如,如果需要,你可以将其替换为另一个解决方案,因为它不会施加重型抽象)。无论如何,Bun的轨迹是向上的,它填补了一个特定的利基(性能导向的ORM),这给了它持久力。 -
sqlc: sqlc在Go社区中非常受欢迎,有约15,900颗星,并且在性能意识圈子里有很多倡导者。它经常在“避免使用ORM”的讨论中被推荐,因为它找到了一个很好的平衡点。该工具由贡献者维护(包括原始作者,他积极改进它)。由于它更像是一个编译器而不是运行时库,其生态系统围绕集成:例如,编辑器/IDE可以对
.sql
文件进行语法高亮,并且你可以在构建或CI管道中运行sqlc generate
。社区创建了模板和基础示例,说明如何使用sqlc组织代码(通常与迁移工具如Flyway或Golang-Migrate配对进行模式版本控制,因为sqlc本身不管理模式)。有一个官方的Slack/Discord用于sqlc,你可以在那里提问,GitHub上的问题通常会得到关注。许多常见模式(如如何处理可空值或JSON字段)在sqlc的文档或社区博客文章中都有记录。需要强调的一点是:sqlc不是Go特定的——它可以生成其他语言(如TypeScript、Python)的代码。这扩展了它的社区范围,但就Go而言,它受到广泛尊重。如果你选择sqlc,你将与许多初创公司和大型公司一起使用它(根据社区展示和仓库中列出的赞助商)。关键的生态系统考虑是,由于sqlc不提供ORM的运行时功能,你可能需要引入其他库来处理事务(尽管你可以轻松使用sql.Tx
与sqlc)或可能使用轻量级DAL包装器。实际上,大多数人会将sqlc与标准库一起使用(对于事务、上下文取消等,你可以在代码中直接使用database/sql
的惯用法)。这意味着更少的供应商锁定——离开sqlc只需编写自己的数据层,这与编写SQL一样困难(你已经完成了)。总体而言,sqlc的社区和支持是稳健的,许多人推荐它作为Go项目与SQL数据库交互时的“必须使用”,因为它简单可靠。
功能集与可扩展性
这些工具都提供了不同的功能集。在这里我们比较它们的能力以及它们在高级用例中的可扩展性:
- GORM功能: GORM的目标是成为全功能ORM。其功能列表非常广泛:
- 多数据库支持: 对PostgreSQL、MySQL、SQLite、SQL Server等提供一流的支撑。切换数据库通常只需更改连接驱动即可。
- 迁移: 你可以从模型自动迁移模式(
db.AutoMigrate(&User{})
将创建或修改users
表)。虽然方便,但请注意在生产环境中使用自动迁移时要小心——许多人在开发中使用它,而在生产中使用更受控的迁移。 - 关系: GORM的结构标签(
gorm:"foreignKey:...,references:..."
)允许你定义一对多、多对多等关系。它可以处理多对多关系的链接表,并有Preload
用于急加载关系。GORM默认在访问相关字段时使用延迟加载(即单独查询),但你可以使用Preload
或Joins
自定义这一点。 新版本还具有基于泛型的API,用于更简单的关联查询。 - 事务: GORM有一个易于使用的事务包装器(
db.Transaction(func(tx *gorm.DB) error { ... })
),甚至支持嵌套事务(保存点)。 - 钩子和回调: 你可以在模型结构上定义方法如
BeforeCreate
、AfterUpdate
,或注册GORM在特定生命周期事件中调用的全局回调。 这对于自动设置时间戳或软删除行为非常有用。 - 可扩展性: GORM的插件系统(
gorm.Plugin
接口)允许扩展其功能。例如:gorm-gen
包生成类型安全的查询方法(如果你喜欢编译时查询检查),或社区插件用于审计、多租户等。你也可以随时通过db.Raw("SELECT ...", params).Scan(&result)
回退到原始SQL。 - 其他便利功能: GORM支持复合主键、嵌入模型、多态关联,甚至一个模式解析器用于使用多个数据库(用于读取副本、分片等)。
总体而言,GORM是高度可扩展且功能丰富的。几乎所有你可能想要的ORM相关功能在GORM中都有内置机制或文档中的模式。这种广度的代价是复杂性和一些刚性(你通常需要遵循GORM的方式做事)。但如果你需要一站式解决方案,GORM可以提供。
- Ent功能: Ent的哲学围绕代码即模式。关键功能包括:
- 模式定义: 你可以定义带有约束(唯一、默认值、枚举等)的字段,以及带有基数(一对多等)的边(关系)。Ent使用这些来生成代码,还可以为你生成迁移SQL(有一个
ent/migrate
组件,可以生成当前模式和目标模式之间的差异SQL)。 - 类型安全和验证: 由于字段是强类型的,你不能,例如,将整数字段设置为字符串。Ent还允许自定义字段类型(例如,你可以与
sql.Scanner
/driver.Valuer
集成,用于JSONB字段或其他复杂类型)。 - 查询构建器: Ent生成的查询API涵盖了大多数SQL构造:你可以进行选择、过滤、排序、限制、聚合、跨边连接,甚至子查询。它表达性强——例如,你可以写
client.User.Query().WithOrders(func(q *ent.OrderQuery) { q.Limit(5) }).Where(user.StatusEQ(user.StatusActive)).All(ctx)
来一次性获取所有活跃用户及其前5个订单。 - 事务: Ent的客户端通过暴露客户端的事务变体来支持事务(通过
tx, err := client.Tx(ctx)
,它会生成一个ent.Tx
,你可以用它来进行多个操作,然后提交或回滚)。 - 钩子和中间件: Ent允许在创建/更新/删除操作上注册钩子——这些类似于拦截器,你可以在其中自动填充字段或强制执行自定义规则。还有客户端的中间件用于包装操作(对日志、监控很有用)。
- 可扩展性: 虽然Ent本身没有“插件”,但其代码生成是模板化的,你可以编写自定义模板来扩展生成的代码。许多高级功能都是通过这种方式实现的:例如,与OpenTelemetry集成以追踪数据库调用,或从Ent模式生成GraphQL解析器等。Ent还允许在必要时通过
entsql
包或获取底层驱动来混合原始SQL。生成额外代码的能力意味着团队可以将Ent作为基础,并在其上添加自己的模式(如果需要)。 - GraphQL/REST集成: Ent的基于图的方法的一个大卖点是它与GraphQL很好地契合——你的ent模式几乎可以直接映射到GraphQL模式。存在工具可以自动化很多这些内容。如果你正在构建一个API服务器,这可以提高生产力。
- 性能优化: 例如,Ent可以批量加载相关边以避免N+1查询(它有一个急加载API
.With<EdgeName>()
)。它会在内部以优化的方式使用JOIN或额外查询。此外,可以启用查询结果的缓存(内存中)以避免在短时间内重复查询数据库。
总结来说,Ent的功能集是面向大规模、可维护项目的。它可能缺少一些GORM的开箱即用的好东西(例如,GORM的自动模式迁移与Ent的生成迁移脚本——Ent选择将该问题分开),但它通过强大的开发工具和类型安全来弥补。在Ent中,可扩展性是关于生成你需要的内容——如果你愿意深入研究entc(代码生成器)的工作方式,它非常灵活。
- Bun功能: Bun专注于成为SQL高手的强力工具。一些功能和可扩展性点:
- 查询构建器: Bun的核心是流畅的查询构建器,支持大多数SQL构造(带连接的SELECT、CTE、子查询,以及带批量支持的INSERT、UPDATE)。你可以启动一个查询并深度自定义它,甚至注入原始SQL(
.Where("some_condition (?)", value)
)。 - PostgreSQL特定功能: Bun内置支持PostgreSQL数组类型、JSON/JSONB(映射到
[]<type>
或map[string]interface{}
或自定义类型),并支持扫描到复合类型。例如,如果你有一个JSONB列,你可以使用json:
标签将其映射到Go结构,Bun将为你处理序列化。这比一些ORM将JSONB视为不透明的更强。 - 关系/急加载: 如前所述,Bun允许你定义结构标签用于关系,然后在查询中使用
.Relation("FieldName")
来连接和加载相关实体。 默认情况下,.Relation
使用LEFT JOIN(你可以通过添加条件来模拟内连接或其他类型)。 这让你对如何获取相关数据有细粒度的控制(在一个查询中还是多个查询中)。如果你更喜欢手动查询,你可以在Bun的SQL构建器中直接编写一个连接。与GORM不同,Bun不会在你没有请求的情况下自动加载关系,这避免了无意中出现N+1问题。 - 迁移: Bun提供了一个单独的迁移工具包(在
github.com/uptrace/bun/migrate
中)。迁移在Go(或SQL字符串)中定义并版本化。它不像GORM的自动迁移那样自动化,但很稳健,并且与库很好地集成。 这对于保持模式更改的明确性非常有用。 - 可扩展性: Bun是基于类似于
database/sql
的接口构建的——它甚至包装了*sql.DB
。因此,你可以使用任何底层工具与它一起使用(例如,如果需要,你可以执行*pgx.Conn
查询并仍然扫描到Bun模型)。Bun允许自定义值扫描器,因此如果你有一个复杂的类型想要存储,你可以实现sql.Scanner
/driver.Valuer
,Bun将使用它。为了扩展Bun的功能,由于相对简单,许多人只是在项目中编写额外的辅助函数在Bun的API之上,而不是独立的插件。库本身正在发展,因此维护者正在添加新功能(如额外的查询助手或集成)。 - 其他功能: Bun支持上下文取消(所有查询接受
ctx
),它通过database/sql
提供连接池(可在其中配置),并支持预处理语句缓存(通过驱动,如果使用pgx
)。还有一个不错的功能是Bun可以轻松扫描到结构指针或原始切片(例如,直接将一个列选择到[]string
中)。这些小便利使得Bun对于那些不喜欢重复扫描代码的人非常愉快。
总结来说,Bun的功能集覆盖了90%的ORM需求,但强调透明度和性能。它可能没有所有花哨的功能(例如,它不进行自动模式更新或有活动记录模式),但它提供了构建你需要的任何内容的构建块。由于它还年轻,预计其功能集会继续扩展,由实际使用案例引导(维护者经常在用户请求时添加功能)。
- sqlc功能: sqlc的“功能”非常不同,因为它是一个生成器:
- 完整的SQL支持: 最大的功能是你可以直接使用PostgreSQL自己的功能。如果Postgres支持,你可以在sqlc中使用它。这包括CTE、窗口函数、JSON操作符、空间查询(PostGIS)等。不需要库本身实现任何特殊功能来使用这些。
- 类型映射: sqlc在将SQL类型映射到Go类型方面很聪明。它处理标准类型,并允许你配置或扩展映射以支持自定义类型或枚举。例如,Postgres的
UUID
可以映射到github.com/google/uuid
类型(如果你需要),或域类型可以映射到底层Go类型。 - 空值处理: 它可以生成
sql.NullString
或指针,根据你的偏好处理可空列,因此你不必与扫描空值作斗争。 - 批量操作: 虽然sqlc本身不提供高级API用于批量操作,但你当然可以编写一个批量插入SQL并为其生成代码。或者调用存储过程——这是另一个功能:由于它是SQL,你可以利用存储过程或数据库函数,并让sqlc生成Go包装器。
- 多语句查询: 你可以在一个命名查询中放入多个SQL语句,sqlc将按事务执行它们(如果驱动支持),返回最后一个查询的结果或你指定的任何内容。这可以让你在一个调用中执行类似“创建然后选择”的操作。
- 可扩展性: 作为编译器,可扩展性以插件的形式出现,用于新语言或社区贡献以支持新SQL构造。例如,如果出现新的PostgreSQL数据类型,sqlc可以更新以支持映射它。在你的应用程序中,你可能通过编写包装函数来扩展sqlc。由于生成的代码在你的控制之下,你可以修改它——尽管通常你不会这样做,而是调整SQL并重新生成。如果需要,你可以在代码库中始终混合原始
database/sql
调用与sqlc(没有冲突,因为sqlc只是输出一些Go代码)。 - 最小的运行时依赖: sqlc生成的代码通常仅依赖于标准
database/sql
(和特定驱动如pgx)。没有重型运行时库;只是些辅助类型。这意味着从库方面在生产中没有开销——所有工作都在编译时完成。这也意味着你不会得到像对象缓存或身份映射(一些ORM有)这样的功能——如果你需要缓存,你将在服务层实现它或使用单独的库。
实际上,sqlc的“功能”是它缩小了你的SQL数据库和Go代码之间的差距,而没有在中间添加额外的东西。它是一个专用工具——它不进行迁移,不跟踪对象状态等,这是有意为之。这些关注点留给其他工具或开发者。如果你想要一个轻量的数据层,这很吸引人,但如果你正在寻找一个一站式解决方案,从模式到查询到关系在代码中处理一切,sqlc本身不是——你将它与其他模式或工具配对。
总结本节,GORM是功能的强大力量,如果你需要一个成熟、可扩展的ORM,可以通过插件进行调整,它是一个明确的选择。Ent提供了现代、类型安全的功能,优先考虑正确性和可维护性(通过代码生成、钩子和集成到API层)。Bun提供了基本功能,并依靠SQL熟练度,使其通过编写更多SQL或轻微包装器来扩展变得容易。sqlc将功能缩减到最基础——它本质上与SQL本身一样功能丰富,但任何超出部分(缓存等)都由你来添加。
代码示例:每个 ORM 中的 CRUD 操作
没有什么比看到每个工具如何处理一个简单模型的基本 CRUD 操作更能说明差异了。假设我们有一个 User
模型,包含字段 ID
、Name
和 Email
。下面是在每个库中创建、读取、更新和删除用户时的并排代码片段,使用 PostgreSQL 作为数据库。
注意:在所有情况下,假设我们已经建立了一个数据库连接(例如 db
或 client
),并且为了简洁省略了错误处理。目的是比较每种方法的 API 和冗长程度。
GORM(Active Record 风格)
// 模型定义
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Email string
}
// 创建一个新用户
user := User{Name: "Alice", Email: "alice@example.com"}
db.Create(&user) // INSERT INTO users (name,email) VALUES ('Alice','alice@example.com')
// 读取(通过主键查找)
var u User
db.First(&u, user.ID) // SELECT * FROM users WHERE id = X LIMIT 1
// 更新(单个列)
db.Model(&u).Update("Email", "alice_new@example.com")
// (生成:UPDATE users SET email='alice_new@example.com' WHERE id = X)
// 删除
db.Delete(&User{}, u.ID) // DELETE FROM users WHERE id = X
Ent(代码生成,流畅 API)
//(Ent 模式在别处定义并生成代码。假设 client 是 ent.Client)
// 创建一个新用户
u, err := client.User.
Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
// SQL: INSERT INTO users (name, email) VALUES ('Alice','alice@example.com') RETURNING id, name, email
// 读取(通过 ID)
u2, err := client.User.Get(ctx, u.ID)
// SQL: SELECT * FROM users WHERE id = X
// 更新(单个字段)
u3, err := client.User.
UpdateOneID(u.ID).
SetEmail("alice_new@example.com").
Save(ctx)
// SQL: UPDATE users SET email='alice_new@example.com' WHERE id = X RETURNING id, name, email
// 删除
err = client.User.DeleteOneID(u.ID).Exec(ctx)
// SQL: DELETE FROM users WHERE id = X
Bun
// 模型定义
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique,notnull"`
}
// 创建一个新用户
user := &User{Name: "Alice", Email: "alice@example.com"}
_, err := db.NewInsert().Model(user).Exec(ctx)
// INSERT INTO users (name, email) VALUES ('Alice','alice@example.com')
// 读取(通过 ID 查找)
var u User
err = db.NewSelect().Model(&u).
Where("id = ?", user.ID).
Scan(ctx)
// SELECT * FROM users WHERE id = X
// 更新(单个列)
_, err = db.NewUpdate().Model(&u).
Set("email = ?", "alice_new@example.com").
Where("id = ?", u.ID).
Exec(ctx)
// UPDATE users SET email='alice_new@example.com' WHERE id = X
// 删除
_, err = db.NewDelete().Model((*User)(nil)).
Where("id = ?", u.ID).
Exec(ctx)
// DELETE FROM users WHERE id = X
sqlc
// 假设我们在文件中编写了 SQL,并且 sqlc 生成了一个带有方法的 Queries 结构体。
queries := New(db) // New 接收 *sql.DB(或 pgx.Conn)并返回生成的 Queries
// 创建一个新用户(生成的方法执行 INSERT 并扫描结果)
newUser, err := queries.CreateUser(ctx, "Alice", "alice@example.com")
// queries.sql 中的 SQL -> INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email
// 读取(通过 ID)
user, err := queries.GetUser(ctx, newUser.ID)
// SQL -> SELECT id, name, email FROM users WHERE id = $1
// 更新(通过 ID 更新电子邮件)
updatedUser, err := queries.UpdateUserEmail(ctx, newUser.ID, "alice_new@example.com")
// SQL -> UPDATE users SET email=$2 WHERE id = $1 RETURNING id, name, email
// 删除
err = queries.DeleteUser(ctx, newUser.ID)
// SQL -> DELETE FROM users WHERE id = $1
如上面的代码所示,每种方法都有不同的感觉:
-
GORM 使用结构体方法和在
db.Model(&obj)
或直接在db
对象上的流畅链式调用。它用返回的值填充结构体(例如,在Create
之后,user.ID
会被设置)。它通过设计隐藏了 SQL 详细信息——除非调试,否则你通常看不到查询。 -
Ent 使用生成的流畅 API。请注意像
Create().SetX().Save(ctx)
或UpdateOneID(id).SetX().Save(ctx)
这样的方法如何清楚地将构建阶段与执行阶段分开。Ent 返回 ent.Type 对象(对应行)和错误,类似于 SQL 查询要么返回结果要么返回错误。 -
Bun 需要更明确地指定(例如,使用
Set("email = ?", ...)
进行更新),这非常类似于编写 SQL 但使用 Go 语法。插入后,除非添加RETURNING
子句(Bun 支持.Returning()
如果需要),否则结构体user
不会自动填充新 ID。上面的示例保持简单。 -
sqlc 看起来就像调用任何 Go 函数。我们调用
queries.CreateUser
等,而底层这些是执行预处理语句。SQL 写在外部文件中,因此虽然你不会在 Go 代码中看到它,但你可以完全控制它。返回的对象(例如newUser
)是 sqlc 生成的普通 Go 结构体,用于建模数据。
可以观察到冗长程度的差异(GORM 非常简洁;Bun 和 sqlc 在代码或 SQL 中需要多打一些字)和风格差异(Ent 和 GORM 提供更高层次的抽象,而 Bun 和 sqlc 更接近原始查询)。根据你的偏好,你可能更倾向于显式性胜过简洁性,反之亦然。
TL;DR
在 Go 中选择“正确的”ORM 或数据库库取决于你的应用程序需求和团队偏好:
-
GORM 是一个很好的选择,如果你想要一个经过验证的 ORM,它为你处理很多内容。它在快速开发 CRUD 应用程序时表现出色,其中便利性和丰富的功能集比榨取每一点性能更重要。社区支持和文档非常出色,可以平滑地克服其学习曲线的粗糙边缘。请注意适当使用其功能(例如,使用
Preload
或Joins
来避免懒加载的陷阱),并预期一些开销。作为交换,你获得生产力和大多数问题的一站式解决方案。 如果你需要诸如多数据库支持或广泛的插件生态系统等功能,GORM 会满足你的需求。 -
Ent 吸引那些优先考虑类型安全、清晰度和可维护性的人。它非常适合大型代码库,其中模式更改频繁,你希望编译器在你身边捕捉错误。Ent 可能需要更多的前期设计(定义模式、运行生成),但随着项目的增长,它会得到回报——你的代码保持稳健且易于重构。性能方面,它可以高效处理重负载和复杂查询,通常比活动记录 ORM 更好,这得益于其优化的 SQL 生成和缓存。 Ent 对于快速脚本来说稍微“即插即用”性稍差,但对于长期服务,它提供了坚实、可扩展的基础。其现代方法(和活跃开发)使其成为一个面向未来的选择。
-
Bun 是为那些说“我知道 SQL,我只是想要一个轻量级助手”的开发者而设计的。它放弃了一些魔法,以给你控制和速度。如果你正在构建一个对性能敏感的服务,并且不怕在数据访问代码中显式编写,Bun 是一个有吸引力的选择。它也是从原始
database/sql
+sqlx
迁移并希望添加结构而不牺牲太多效率的一个良好中间地带。权衡是社区较小和较少的高层抽象——你将手动编写更多代码。但正如 Bun 文档所说,它不会妨碍你。 当你想要一个感觉像直接使用 SQL 的 ORM 时使用 Bun,尤其是对于以 PostgreSQL 为中心的项目,你可以利用数据库特定的功能。 -
sqlc 是一个独特的类别。它非常适合 Go 团队,他们说“我们不需要 ORM,我们想要编译时对 SQL 的保证。”如果你有强大的 SQL 技能,并且喜欢在 SQL 中管理查询和模式(也许你有 DBA 或者喜欢编写高效的 SQL),sqlc 很可能会提高你的生产力和信心。性能本质上是最佳的,因为没有任何东西在你的查询和数据库之间。不使用 sqlc 的唯一原因是你真的讨厌编写 SQL 或者你的查询太动态,以至于编写许多变体变得负担沉重。即使如此,你可能会用 sqlc 处理大多数静态查询,并用其他方法处理少数动态情况。sqlc 与其他工具也兼容良好——它不排斥在项目某些部分使用 ORM(有些项目使用 GORM 处理简单任务,而使用 sqlc 处理关键路径)。简而言之,选择 sqlc 如果你重视显式性、零开销和类型安全的 SQL——它是一个强大的工具。
最后,值得注意的是,这些工具在生态系统中并不是互斥的。它们各有优缺点,而在 Go 的务实精神下,许多团队在具体情况下评估权衡。从使用 GORM 或 Ent 进行快速开发开始,然后使用 sqlc 或 Bun 处理需要最大性能的特定热点路径是不罕见的。所有四个解决方案都积极维护并广泛使用,因此总体上没有“错误”的选择——它取决于你的上下文。希望这个比较能给你一个更清晰的画面,说明 GORM、Ent、Bun 和 sqlc 如何相互比较,并帮助你在下一个 Go 项目中做出明智的决策。