Comparando ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc

Una mirada práctica y con mucho código sobre ORMs en GO

Índice

Los ORMs más prominentes para GO son GORM, Ent, Bun y sqlc. Aquí hay una pequeña comparación entre ellos con ejemplos de operaciones CRUD en GO puro.

golang + postgresql

TL;DR

  • GORM: lleno de características y conveniente; más fácil de “solo enviar”, pero tiene más sobrecarga en tiempo de ejecución.
  • Ent: esquema como código con APIs generadas y seguras por tipo; excelente para grandes bases de código y refactorizaciones.
  • Bun: ligero, constructor de consultas/ORM basado en SQL; rápido con excelentes características de Postgres, explícito por diseño.
  • sqlc (no es un ORM en sí mismo pero aún así): escribe SQL, obtén Go seguro por tipo; mejor rendimiento crudo y control, sin magia en tiempo de ejecución.

Criterios de selección y comparación rápida

Mis criterios son:

  • Rendimiento: latencia/rendimiento, sobrecarga evitable, operaciones por lotes.
  • DX: curva de aprendizaje, seguridad por tipo, depurabilidad, fricción de generación de código.
  • Ecosistema: documentación, ejemplos, actividad, integraciones (migraciones, seguimiento).
  • Conjunto de características: relaciones, carga anticipada, migraciones, ganchos, escapes de SQL crudo.
Herramienta Paradigma Seguridad por tipo Relaciones Migraciones Ergonomía de SQL crudo Caso de uso típico
GORM ORM estilo ActiveRecord Media (en tiempo de ejecución) Sí (etiquetas, Preload/Joins) Migrar automáticamente (opcional) db.Raw(...) Entrega rápida, características ricas, aplicaciones CRUD convencionales
Ent Esquema → generación de código → API fluida Alta (en tiempo de compilación) Primera clase (aristas) SQL generado (paso separado) entsql, SQL personalizado Grandes bases de código, equipos con refactorizaciones frecuentes, tipado estricto
Bun Constructor de consultas/ORM basado en SQL Media–Alta Explícita (Relation) Paquete separado para migraciones Natural (constructor + crudo) Servicios orientados al rendimiento, características de Postgres
sqlc SQL → funciones generadas (no es un ORM) Alta (en tiempo de compilación) Vía uniónes SQL Herramienta externa (ej., golang-migrate) Es SQL Máximo control y velocidad; equipos amigables con DBA

CRUD por ejemplo

Configuración (PostgreSQL)

Usa pgx o el controlador nativo de PG de la herramienta. Ejemplo DSN:

export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'

Importaciones (comunes para todos los ORMs)

Al inicio de cada archivo con código GO ejemplo agrega:

import (
  "context"
  "os"
)

Modelaremos una tabla simple users:

CREATE TABLE IF NOT EXISTS users (
  id    BIGSERIAL PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

GORM

Iniciar

import (
  "gorm.io/driver/postgres"
  "gorm.io/gorm"
)

type User struct {
  ID    int64  `gorm:"primaryKey"`
  Name  string
  Email string `gorm:"uniqueIndex"`
}

func newGorm() (*gorm.DB, error) {
  dsn := os.Getenv("DATABASE_URL")
  return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}

// Migrar automáticamente (opcional; ten cuidado en producción)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

func gormCRUD(ctx context.Context, db *gorm.DB) error {
  // Crear
  u := User{Name: "Alice", Email: "alice@example.com"}
  if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }

  // Leer
  var got User
  if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }

  // Actualizar
  if err := db.WithContext(ctx).Model(&got).
    Update("email", "alice+1@example.com").Error; err != nil { return err }

  // Eliminar
  if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }

  return nil
}

Notas

  • Relaciones mediante etiquetas de estructura + Preload/Joins.
  • Helper de transacción: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Definición del esquema (en ent/schema/user.go):

package schema

import (
  "entgo.io/ent"
  "entgo.io/ent/schema/field"
)

type User struct {
  ent.Schema
}

func (User) Fields() []ent.Field {
  return []ent.Field{
    field.Int64("id").Unique().Immutable(),
    field.String("name"),
    field.String("email").Unique(),
  }
}

Generar código

go run entgo.io/ent/cmd/ent generate ./ent/schema

Iniciar

import (
  "entgo.io/ent/dialect"
  "entgo.io/ent/dialect/sql"
  _ "github.com/jackc/pgx/v5/stdlib"
  "your/module/ent"
)

func newEnt() (*ent.Client, error) {
  dsn := os.Getenv("DATABASE_URL")
  drv, err := sql.Open(dialect.Postgres, dsn)
  if err != nil { return nil, err }
  return ent.NewClient(ent.Driver(drv)), nil
}

CRUD

func entCRUD(ctx context.Context, client *ent.Client) error {
  // Crear
  u, err := client.User.Create().
    SetName("Alice").
    SetEmail("alice@example.com").
    Save(ctx)
  if err != nil { return err }

  // Leer
  got, err := client.User.Get(ctx, u.ID)
  if err != nil { return err }

  // Actualizar
  if _, err := client.User.UpdateOneID(got.ID).
    SetEmail("alice+1@example.com").
    Save(ctx); err != nil { return err }

  // Eliminar
  if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }

  return nil
}

Notas

  • Tipado fuerte de extremo a extremo; aristas para relaciones.
  • Migraciones generadas o usa tu herramienta de migración de elección.

Bun

Iniciar

import (
  "database/sql"

  "github.com/uptrace/bun"
  "github.com/uptrace/bun/dialect/pgdialect"
  _ "github.com/jackc/pgx/v5/stdlib"
)

type User struct {
  bun.BaseModel `bun:"table:users"`
  ID    int64  `bun:",pk,autoincrement"`
  Name  string `bun:",notnull"`
  Email string `bun:",unique,notnull"`
}

func newBun() (*bun.DB, error) {
  dsn := os.Getenv("DATABASE_URL")
  sqldb, err := sql.Open("pgx", dsn)
  if err != nil { return nil, err }
  return bun.NewDB(sqldb, pgdialect.New()), nil
}

CRUD

func bunCRUD(ctx context.Context, db *bun.DB) error {
  // Crear
  u := &User{Name: "Alice", Email: "alice@example.com"}
  if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }

  // Leer
  var got User
  if err := db.NewSelect().Model(&got).
    Where("id = ?", u.ID).
    Scan(ctx); err != nil { return err }

  // Actualizar
  if _, err := db.NewUpdate().Model(&got).
    Set("email = ?", "alice+1@example.com").
    WherePK().
    Exec(ctx); err != nil { return err }

  // Eliminar
  if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }

  return nil
}

Notas

  • Uniones/eager loading explícitas con .Relation("...").
  • Paquete separado bun/migrate para migraciones.

sqlc

sqlc técnicamente no es un ORM. Escribe SQL; genera métodos de Go seguros por tipo.

sqlc.yaml

version: "2"
sql:
  - engine: postgresql
    queries: db/queries
    schema: db/migrations
    gen:
      go:
        package: db
        out: internal/db
        sql_package: "database/sql" # o "github.com/jackc/pgx/v5"

Consultas (db/queries/users.sql)

-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email;

-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;

-- name: UpdateUserEmail :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING id, name, email;

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

Generar

sqlc generate

Uso

import (
  "database/sql"
  _ "github.com/jackc/pgx/v5/stdlib"
  "your/module/internal/db"
)

func sqlcCRUD(ctx context.Context) error {
  dsn := os.Getenv("DATABASE_URL")
  sqldb, err := sql.Open("pgx", dsn)
  if err != nil { return err }
  q := db.New(sqldb)

  // Crear
  u, err := q.CreateUser(ctx, db.CreateUserParams{
    Name: "Alice", Email: "alice@example.com",
  })
  if err != nil { return err }

  // Leer
  got, err := q.GetUser(ctx, u.ID)
  if err != nil { return err }

  // Actualizar
  up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
    ID: got.ID, Email: "alice+1@example.com",
  })
  if err != nil { return err }

  // Eliminar
  if err := q.DeleteUser(ctx, up.ID); err != nil { return err }

  return nil
}

Notas

  • Trae tus propias migraciones (ej., golang-migrate).
  • Para consultas dinámicas: escribe varias variantes de SQL o combínalas con un pequeño constructor.

Notas sobre el rendimiento

  • GORM: conveniente pero añade sobrecarga de reflexión/abstracción. Bien para CRUD típico; ten cuidado con consultas N+1 (prefiere Joins o Preload selectivo).
  • Ent: el código generado evita la reflexión; bueno para esquemas complejos. A menudo más rápido que ORMs pesados con magia en tiempo de ejecución.
  • Bun: delgado sobre database/sql; rápido, explícito, excelente para operaciones por lotes y grandes conjuntos de resultados.
  • sqlc: rendimiento esencialmente crudo de SQL con seguridad en tiempo de compilación.

Consejos generales

  • Usa pgx como controlador (v5) y context en todas partes.
  • Prefiere operaciones por lotes (COPY, INSERT de múltiples filas) para alto rendimiento.
  • Perfil SQL: EXPLAIN ANALYZE, índices, índices cubiertos, evita viajes innecesarios.
  • Reutiliza conexiones; ajusta el tamaño de la piscina según la carga de trabajo.

Experiencia del desarrollador y ecosistema

  • GORM: comunidad más grande, muchos ejemplos/plugins; curva de aprendizaje más empinada para patrones avanzados.
  • Ent: excelentes documentaciones; el paso de generación de código es el cambio principal de mentalidad; super amigable para refactorizaciones.
  • Bun: consultas legibles y predecibles; comunidad más pequeña pero activa; excelentes niceties de Postgres.
  • sqlc: dependencias mínimas en tiempo de ejecución; se integra bien con herramientas de migración y CI; excelente para equipos cómodos con SQL.

Destacados de características

  • Relaciones y carga anticipada: todos manejan relaciones; GORM (etiquetas + Preload/Joins), Ent (aristas + .With...()), Bun (Relation(...)), sqlc (escribes las uniónes).
  • Migraciones: GORM (migrar automáticamente; ten cuidado en producción), Ent (SQL generado/diff), Bun (bun/migrate), sqlc (herramientas externas).
  • Ganchos/Extensibilidad: GORM (callbacks/plugins), Ent (ganchos/middleware + plantilla/generación de código), Bun (ganchos de consulta similares a middleware, SQL crudo fácil), sqlc (componer en capa de aplicación).
  • JSON/Arrays (Postgres): Bun y GORM tienen ayudantes útiles; Ent/sqlc manejan mediante tipos personalizados o SQL.

Cuándo elegir qué

  • Elige GORM si quieres máxima conveniencia, características ricas y prototipado rápido para servicios CRUD convencionales.
  • Elige Ent si valoras la seguridad en tiempo de compilación, esquemas explícitos y mantenibilidad a largo plazo en equipos grandes.
  • Elige Bun si quieres rendimiento y consultas SQL explícitas con comodidades de ORM donde sea útil.
  • Elige sqlc si tú y tu equipo prefieren SQL puro con enlaces seguros de Go y cero sobrecarga en tiempo de ejecución.

docker-compose.yml mínimo para PostgreSQL local

version: "3.8"
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 5s
      timeout: 3s
      retries: 5

Paquetes y bibliotecas de ORM en GO

Otros enlaces útiles