Comparación de ORMs de Go para PostgreSQL: GORM frente a Ent, Bun y sqlc

Una visión práctica y centrada en el código sobre las ORMs en Go

Índice

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

golang + postgresql

TL;DR

  • GORM: repleto de funciones y conveniente; el más fácil para “solo lanzar el producto”, pero tiene más sobrecarga en tiempo de ejecución.
  • Ent: esquema como código con APIs generadas y seguras en cuanto a tipos; excelente para bases de código grandes y refactorizaciones.
  • Bun: ligero, creador de consultas/ORM centrado en SQL; rápido con excelentes funciones de Postgres, explícito por diseño.
  • sqlc (no es estrictamente un ORM, pero aun así): escribes SQL y obtienes Go seguro en cuanto a tipos; mejor rendimiento y control crudos, sin magia en tiempo de ejecución.

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

Mis criterios son:

  • Rendimiento: latencia/throughput, sobrecarga evitable, operaciones por lotes (batch).
  • Experiencia de desarrollo (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 funciones: relaciones, carga anticipada (eager loading), migraciones, hooks, escapes a SQL crudo.
Herramienta Paradigma Seguridad de tipos Relaciones Migraciones Ergonomía de SQL crudo Caso de uso típico
GORM ORM de estilo Active Record Media (tiempo de ejecución) Sí (etiquetas, Preload/Joins) Migración automática (opt-in) db.Raw(...) Entrega rápida, funciones ricas, aplicaciones CRUD convencionales
Ent Esquema → generación de código → API fluida Alta (compilación) De primera clase (aristas) SQL generado (paso separado) entsql, SQL personalizado Bases de código grandes, equipos con muchas refactorizaciones, tipado estricto
Bun Creador de consultas/ORM centrado en SQL Media–Alta Explícitas (Relation) Paquete de migración separado Natural (builder + crudo) Servicios con conciencia del rendimiento, funciones de Postgres
sqlc SQL → funciones generadas de código (no es un ORM) Alta (compilación) A través de joins SQL Herramienta externa (p. ej., golang-migrate) Es SQL Máximo control y velocidad; equipos amigables con DBA

CRUD con ejemplos

Configuración (PostgreSQL)

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

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

Importaciones (comunes para todos los ORMs)

Al principio de cada archivo con código go agregas:

import (
  "context"
  "os"
)

Modelaremos una simple tabla users:

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.
  • Asistente 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 principio a fin; aristas 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

  • Joins/carga anticipada explícita con .Relation("...").
  • Paquete separado bun/migrate para migraciones.

sqlc

sqlc técnicamente no es un ORM. Escribes SQL; genera métodos de Go seguros en cuanto a 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 (p. ej., golang-migrate).
  • Para consultas dinámicas: escribe múltiples variantes de SQL o combínalas con un pequeño creador de consultas.

Notas de rendimiento

  • GORM: conveniente pero agrega sobrecarga de reflexión/abstracción. Bien para CRUD típico; 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 los ORMs pesados con magia en tiempo de ejecución.
  • Bun: delgado sobre database/sql; rápido, explícito, excelente para operaciones por lotes y conjuntos de resultados grandes.
  • sqlc: esencialmente rendimiento de SQL crudo con seguridad en tiempo de compilación.

Consejos generales

  • Usa pgx para el conductor (v5) y context en todas partes.
  • Prefiere el procesamiento por lotes (COPY, INSERT de varias filas) para alto rendimiento.
  • Perfil SQL: EXPLAIN ANALYZE, índices, índices de cobertura, evita recorridos 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: excelente documentación; el paso de generación de código es el cambio principal en el modelo mental; súper amigable con refactorizaciones.
  • Bun: consultas legibles y predecibles; comunidad más pequeña pero activa; excelentes características 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 funciones

  • Relaciones y carga anticipada: todos manejan relaciones; GORM (etiquetas + Preload/Joins), Ent (aristas + .With...()), Bun (Relation(...)), sqlc (tú escribes los joins).
  • Migraciones: GORM (migración automática; cuidado en producción), Ent (SQL generado/diferenciado), Bun (bun/migrate), sqlc (herramientas externas).
  • Hooks/Extensibilidad: GORM (callbacks/plugins), Ent (hooks/middleware + plantilla/generación de código), Bun (hooks de consulta tipo middleware, SQL crudo fácil), sqlc (componer en tu capa de aplicación).
  • JSON/Arrays (Postgres): Bun y GORM tienen buenos asistentes; Ent/sqlc los manejan a través de tipos personalizados o SQL.

Una nota transversal para los cuatro: Go 1.27 ahora respalda encoding/json con el motor v2, lo que preserva el comportamiento pero cambia algunos textos de error y, bajo la API v2 explícita, la salida de slices nil y claves de mapa. Si tus modelos hacen ida y vuelta de JSON, revisa lo que dice la guía de actualización de Go 1.27 sobre los fixtures de pruebas doradas antes de actualizar.

Cuándo elegir qué

  • Elige GORM si quieres conveniencia máxima, funciones 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 con forma de SQL explícita con las comodidades de un ORM donde ayude.
  • Elige sqlc si tú (y tu equipo) prefieren SQL puro con bindings de Go seguros en cuanto a 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 diseñadas para los llamadores en lugar de las 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, esta visión general de la arquitectura de la aplicación ayuda a situar 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.