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.
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
oPreload
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
- Hoja de trucos de Golang
- Rendimiento de AWS lambda: JavaScript vs Python vs Golang
- Corrigiendo el error de GORM AutoMigrate en PostgreSQL
- Reclasificación de documentos de texto con Ollama y modelo de embedding Qwen3 - en Go
- Reclasificación de documentos de texto con Ollama y modelo de reranker Qwen3 - en Go