Comparación de ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc

Una mirada práctica, con abundante código, a los ORM en Go

Índice

Las ORMs más destacadas para Go son GORM, Ent, Bun y sqlc. Aquí hay una pequeña comparación de ellas con ejemplos de operaciones CRUD en Go puro.

golang + postgresql

Resumen (TL;DR)

  • GORM: Llena de funciones y conveniente; es la más fácil para “entregar rápido”, pero tiene más sobrecarga en tiempo de ejecución.
  • Ent: Esquema como código con APIs generadas y seguras en tipos; excelente para bases de código grandes y refactorizaciones.
  • Bun: Constructor de consultas/ORM ligero y orientado a SQL; rápido con grandes características de Postgres, explícito por diseño.
  • sqlc (no es una ORM propiamente dicha, pero aún así): escribe SQL, obtén Go seguro en tipos; el mejor rendimiento y control en bruto, sin magia en tiempo de ejecución.

Criterios de Selección y Comparación Rápida

Mis criterios son:

  • Rendimiento: latencia/ancho de banda, sobrecarga evitable, operaciones por lotes.
  • Experiencia del Desarrollador (DX): curva de aprendizaje, seguridad de tipos, depurabilidad, fricción en la generación de código.
  • Ecosistema: documentación, ejemplos, actividad, integraciones (migraciones, trazabilidad).
  • Conjunto de características: relaciones, carga ansiosa (eager loading), migraciones, ganchos (hooks), escapes para SQL en bruto.
Herramienta Paradigma Seguridad de Tipos Relaciones Migraciones Ergonomía de SQL en Bruto Caso de Uso Típico
GORM ORM estilo Active Record Media (en tiempo de ejecución) Sí (etiquetas, Preload/Joins) Auto-migración (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) De primera clase (bordes/aristas) SQL generado (paso separado) entsql, SQL personalizado Bases de código grandes, equipos con muchas refactorizaciones, tipado estricto
Bun Constructor de consultas/ORM orientado a SQL Media-Alta Explícito (Relation) Paquete de migración separado Natural (constructor + en bruto) Servicios conscientes del rendimiento, características de Postgres
sqlc SQL → funciones generadas (no es una ORM) Alta (en tiempo de compilación) Mediante uniones SQL Herramienta externa (ej., golang-migrate) Es SQL Máximo control y velocidad; equipos amigables con DBAs

CRUD por Ejemplo

Configuración (PostgreSQL)

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

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

Importaciones (comunes para todas las ORMs)

Al principio de cada archivo con código go ejemplo, añade:

import (
  "context"
  "os"
)

Modelaremos una tabla users simple:

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

GORM

Inicialización

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{})
}

// Auto-migración (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 transacciones: 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

Inicialización

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; bordes (edges) para relaciones.
  • Migraciones generadas o usa tu herramienta de migración preferida.

Bun

Inicialización

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 explícitas/carga ansiosa con .Relation("...").
  • Paquete separado bun/migrate para migraciones.

sqlc

sqlc técnicamente no es una ORM. Escribes SQL; genera métodos Go seguros en tipos.

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

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

Notas sobre el Rendimiento

  • GORM: conveniente pero añade sobrecarga de reflexión/abstracción. Aceptable para CRUD típico; ten cuidado con las 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: capa delgada sobre database/sql; rápido, explícito, excelente para operaciones por lotes y conjuntos de resultados grandes.
  • sqlc: esencialmente rendimiento de SQL en bruto con seguridad en tiempo de compilación.

Consejos generales

  • Usa pgx para el conductor (v5) y context en todas partes.
  • Prefiere operaciones por lotes (COPY, INSERT multi-fila) para alto ancho de banda.
  • Perfil SQL: EXPLAIN ANALYZE, índices, índices de cobertura, evita viajes de ida y vuelta innecesarios.
  • Reutiliza conexiones; ajusta el tamaño del pool según la carga de trabajo.

Experiencia del Desarrollador y Ecosistema

  • GORM: comunidad más grande, muchos ejemplos/plugin; curva de aprendizaje más pronunciada para patrones avanzados.
  • Ent: excelente documentación; el paso de generación de código es el principal cambio de modelo mental; súper amigable para refactorizaciones.
  • Bun: consultas legibles y predecibles; comunidad más pequeña pero activa; excelentes detalles 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.

Puntos Destacados de las Características

  • Relaciones y carga ansiosa: todos manejan relaciones; GORM (etiquetas + Preload/Joins), Ent (bordes + .With...()), Bun (Relation(...)), sqlc (tú escribes las uniones).
  • Migraciones: GORM (auto-migración; cuidado en producción), Ent (SQL generado/diferencias), Bun (bun/migrate), sqlc (herramientas externas).
  • Ganchos/Extensibilidad: GORM (callbacks/plugins), Ent (ganchos/middleware + plantillas/generación de código), Bun (ganchos de consulta estilo middleware, SQL en bruto fácil), sqlc (compón en tu capa de aplicación).
  • JSON/Arrays (Postgres): Bun y GORM tienen helpers agradables; Ent/sqlc lo 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 más grandes.
  • Elige Bun si quieres rendimiento y consultas en forma de SQL explícitas con comodidades de ORM donde ayudan.
  • Elige sqlc si tú (y tu equipo) prefieren SQL puro con bindings Go seguros en tipos y cero sobrecarga en tiempo de ejecución. sqlc también es una opción natural para el lado del modelo de lectura de una arquitectura CQRS en Go, donde las consultas están moldeadas para los llamadores en lugar de entidades de dominio y el SQL explícito te da control total sobre la proyección.

Si aún estás equilibrando esta elección de ORM contra el estilo de integración y los límites del servicio, este resumen de arquitectura de aplicaciones ayuda a colocar la decisión en un contexto de producción más amplio.


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 librerías ORM en GO

Otros Enlaces Útiles

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.