梦兽编程
AI_SUITE

Go ORM Selection Notes: Is Bun Really Faster Than GORM?

A practical comparison of Bun and GORM for a SQLite sidecar project, covering performance, migration trade-offs, and writing style differences.

Go ORM Selection Notes: Is Bun Really Faster Than GORM?

When refactoring the data layer of a small sidecar project (rex-hugo-sidercat), I had to choose between GORM and Bun. This post records the thinking process, benchmark numbers, and the real trade-offs we found.

1. Why switch away from hand-written SQL?

The project started with raw database/sql and inline schema strings. As tables grew, a few problems surfaced:

  • Field types and structs drifted apart; changing one place often missed another.
  • Raw QueryRow / Exec boilerplate piled up.
  • Schema changes had no version history, so local and production environments could silently diverge.

The goal was clear: bring in a lightweight ORM that is maintainable and gives us controlled schema migrations.

2. Candidates

OptionMaturityLearning curveStyle
GORMHighestGentleChainable object queries, ThinkPHP/Laravel-like
BunMediumGentleCloser to hand-written SQL
EntMediumSteepCode-generation + type safety

The project only has four tables: users, orders, access, and membership. Ent’s code-generation benefit was small; GORM is the most mature but has more “magic”; Bun has a reputation for SQL transparency and performance. So the real comparison became GORM vs Bun.

ORM trade-offs: performance, SQL transparency, ecosystem maturity

3. Syntax: Bun feels more like SQL

GORM uses an object-oriented, chainable style:

var u User
db.Where("phone = ?", phone).First(&u)

db.Model(&User{}).Where("phone = ?", phone).Update("token", newToken)

Bun writes closer to the actual SQL:

var u User
err := db.NewSelect().Model(&u).Where("phone = ?", phone).Scan(ctx)

_, err = db.NewUpdate().
    Model((*User)(nil)).
    Set("token = ?", newToken).
    Where("phone = ?", phone).
    Exec(ctx)

Bun has no First or Save abstractions. The mapping between your code and the final SQL is more direct. If your team prefers the “describe what I want” style of ThinkPHP/Laravel, GORM feels more natural. If you want to keep the SQL in view, Bun is more comfortable.

4. Migrations: Bun has no AutoMigrate

GORM ships with AutoMigrate, which is convenient during development:

db.AutoMigrate(&User{}, &Order{}, &Access{}, &Membership{})

But it is risky in production — easy to drop columns, change types, or create drift between environments.

Bun has no built-in migration tool, so you pair it with a separate one:

ToolStrengthRecommendation
golang-migrateMost popular, SQL files, multi-database⭐⭐⭐
pressly/gooseSupports SQL + Go function migrations⭐⭐⭐
bun/migrateBun-styled, but less community content⭐⭐

The conclusion: regardless of ORM choice, use golang-migrate or goose in production, not AutoMigrate.

5. Performance benchmark

I wrote a small benchmark using the same machine, the same SQLite driver (modernc.org/sqlite), and identical data.

Tests:

  • Single insert
  • Batch insert 1000 rows
  • Single select by unique field
  • Select many (100 rows)
  • Single update

Results (per-operation latency / allocations):

TestGORMBunBun advantage
InsertOne28175 ns/op, 81 allocs14768 ns/op, 25 allocs~1.9x faster, 69% fewer allocs
InsertBatch(1000)7953726 ns/op, 12020 allocs4016759 ns/op, 7187 allocs~2x faster
SelectOne11763 ns/op, 67 allocs10429 ns/op, 37 allocsSlightly faster, fewer allocs
SelectMany(100)161400 ns/op, 1257 allocs148787 ns/op, 1136 allocsSlightly faster
UpdateOne16234 ns/op, 65 allocs6877 ns/op, 15 allocs~2.4x faster, 77% fewer allocs

Bun is noticeably faster on inserts and updates, and slightly faster on selects. The lower allocation count also means less GC pressure.

6. Downsides of Bun

A fair comparison must include the negatives:

  1. Smaller ecosystem than GORM: fewer Chinese-language examples and Stack Overflow answers.
  2. No AutoMigrate: one extra migration step during development.
  3. Lower-level API: if the team is used to GORM’s magic, switching takes adjustment.
  4. Higher hiring/handoff cost: far fewer developers in China know Bun compared to GORM.
  5. No automatic association loading: Bun has no Preload equivalent.

7. Decision

For a small sidecar like rex-hugo-sidercat — four tables, SQLite, WeChat Pay + paid-content permissions:

  • If you prioritize maturity and hiring ease, choose GORM.
  • If you prioritize SQL transparency, performance, and lower reflection overhead, choose Bun and pair it with golang-migrate.

Based on these benchmarks and long-term maintenance, I lean toward Bun + golang-migrate. The schema is small, queries are simple, and the maintainability gain from transparent SQL outweighs GORM’s faster initial development speed.

Frequently Asked Questions

How big is the performance gap between GORM and Bun?

The measured data in the article shows Bun has a clear advantage in insert and update scenarios, and is also slightly better in queries. For example, single insert is about 1.9x faster with Bun, batch insert about 2x faster, single update about 2.4x faster, and memory allocation is lower.

Does Bun support AutoMigrate? How do you handle database migrations?

Bun does not have AutoMigrate, so you need to use a separate migration tool such as golang-migrate, pressly/goose, or bun/migrate. The article recommends using golang-migrate or goose for migrations in production.

What are the differences in coding style between Bun and GORM?

GORM uses an object-oriented chained style and provides highly abstract methods such as First and Save. Bun is closer to the original SQL statements and does not have those methods, so the correspondence between the code you write and the final SQL is more direct.

Why was Ent not a major consideration during the selection process?

Because the project only has four tables: users, orders, access, and membership. The benefit of code generation with Ent is not significant, and its learning curve is steep, so the comparison focused on GORM and Bun.

What is the final recommendation in the article?

For this small sidecar project, the article leans toward choosing Bun with golang-migrate, because the number of tables is small and queries are not complex, so the maintainability gained from SQL transparency outweighs GORM's development efficiency.