PostgreSQL向けのGo ORM比較:GORM vs Ent vs Bun vs sqlc
GoにおけるORMの実践的なコード中心解説
目次
GOにおける最も注目されているORMは、GORM、Ent、Bun、sqlcの4つです。 ここでは、これらを純粋なGOのCRUD操作の例と比較紹介します。

要約(TL;DR)
- GORM: 機能が充実しており便利。「とにかく早くリリースしたい」場合に最適ですが、ランタイムオーバーヘッドがやや大きいです。
- Ent: スキーマをコードとして扱い、型安全なAPIを生成。大規模なコードベースやリファクタリングに向いています。
- Bun: ライトウェイトなSQLファーストなクエリビルダー/ORM。Postgresの機能を活かしつつ高速で、設計意図が明確です。
- sqlc(厳密にはORMではありませんが): SQLを書き、型安全なGoコードを取得。生のSQL同等のパフォーマンスと制御性を備え、ランタイムでのマジック(自動処理)がありません。
選定基準と簡単な比較
私の選定基準は以下の通りです。
- パフォーマンス: レイテンシ/スループット、回避可能なオーバーヘッド、バッチ操作。
- DX(開発者体験): 学習曲線、型安全性、デバッグのしやすさ、コード生成の摩擦。
- エコシステム: ドキュメント、サンプル、活動状況、連携ツール(マイグレーション、トレーシング)。
- 機能セット: リレーション、イージアローディング、マイグレーション、フック、生のSQLへのエスケープハッチ。
| ツール | パラダイム | 型安全性 | リレーション | マイグレーション | 生のSQLの扱いやすさ | 代表的なユースケース |
|---|---|---|---|---|---|---|
| GORM | アクティブレコード風ORM | 中(ランタイム) | 対応(タグ、Preload/Joins) | 自動マイグレーション(オプトイン) | db.Raw(...) |
高速な納品、豊富な機能、標準的なCRUDアプリ |
| Ent | スキーマ → コード生成 → フリューエントAPI | 高(コンパイル時) | 一級市民(edges) | 生成SQL(別ステップ) | entsql、カスタムSQL |
大規模コードベース、リファクタリングが多いチーム、厳格なタイピング |
| Bun | SQLファーストなクエリビルダー/ORM | 中〜高 | 明示的(Relation) |
独立したマイグレーションパッケージ | 自然(ビルダー+生SQL) | パフォーマンス重視サービス、Postgres機能活用 |
| sqlc | SQL → 関数コード生成(ORMではない) | 高(コンパイル時) | SQLのJOIN経由 | 外部ツール(例: golang-migrate) | 生SQLそのもの | 最大限の制御と速度、DBAフレンドリーなチーム |
例によるCRUD
環境構築(PostgreSQL)
pgx またはツール固有のPGドライバーを使用してください。DSNの例:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
インポート(すべてのORMで共通)
go codeの例を持つ各ファイルの先頭に、以下を追加してください:
import (
"context"
"os"
)
シンプルな users テーブルをモデル化します:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
GORM
初期化
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{})
}
// 自動マイグレーション(オプション;本番環境では注意)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }
CRUD
func gormCRUD(ctx context.Context, db *gorm.DB) error {
// Create
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// Read
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// Update
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// Delete
if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }
return nil
}
注記
- リレーションは構造体タグ +
Preload/Joinsで扱います。 - トランザクションヘルパー:
db.Transaction(func(tx *gorm.DB) error { ... })。
Ent
スキーマ定義(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(),
}
}
コード生成
go run entgo.io/ent/cmd/ent generate ./ent/schema
初期化
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 {
// Create
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// Read
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// Update
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// Delete
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
注記
- エンドツーエンドで強力なタイピング;リレーションはedgesを使用。
- 生成されたマイグレーションを使用するか、お好みのマイグレーションツールを使用してください。
Bun
初期化
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 {
// Create
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// Read
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// Update
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// Delete
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
注記
- 明示的なJOIN/イージアローディングは
.Relation("...")を使用。 - マイグレーションは独立した
bun/migrateパッケージを使用。
sqlc
sqlcは技術的にはORMではありません。 SQLを書き、型安全なGoメソッドを生成します。
sqlc.yaml
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: internal/db
sql_package: "database/sql" # or "github.com/jackc/pgx/v5"
クエリ(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;
生成
sqlc generate
使用例
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)
// Create
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// Read
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// Update
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// Delete
if err := q.DeleteUser(ctx, up.ID); err != nil { return err }
return nil
}
注記
- マイグレーションは持ち込み(例:
golang-migrate)。 - 動的クエリの場合:複数のSQLバリアントを書くか、小さなビルダーと組み合わせる。
パフォーマンスに関する注意
- GORM: 便利ですが、リフレクション/抽象化によるオーバーヘッドを追加します。一般的なCRUDには問題ありませんが、N+1クエリには注意してください(
Joinsまたは選択的なPreloadを推奨)。 - Ent: 生成されたコードはリフレクションを回避します;複雑なスキーマに良好。多くの場合、ランタイムのマジックに頼る重量級のORMより高速です。
- Bun:
database/sqlの薄いラッパー;高速で明示的であり、バッチ操作や大きな結果セットに最適です。 - sqlc: 実質的に、コンパイル時の安全性を備えた生SQLのパフォーマンスです。
一般的なヒント
- ドライバーには pgx (v5) を、どこでも context を使用してください。
- 高スループットには、バッチ処理(
COPY、複数行のINSERT)を推奨します。 - SQLをプロファイルしてください:
EXPLAIN ANALYZE、インデックス、カバリングインデックス、不要なラウンドトリップの回避。 - 接続を再利用し、ワークロードに基づいてプールサイズを調整してください。
開発者体験(DX)とエコシステム
- GORM: 最大のコミュニティ、多数の例/プラグイン;高度なパターンには学習曲線が急。
- Ent: 素晴らしいドキュメント;コード生成ステップが主なメンタルモデルの変化;リファクタリングに非常に親しみやすい。
- Bun: 読みやすく予測可能なクエリ;小さめだが活発なコミュニティ;Postgresの優れた機能。
- sqlc: 最小限のランタイム依存;マイグレーションツールやCIとの連携が良好;SQLに慣れたチームにとって最高。
機能ハイライト
- リレーション & イージアローディング: すべてリレーションに対応;GORM(タグ +
Preload/Joins)、Ent(edges +.With...())、Bun(Relation(...))、sqlc(JOINを書き込む)。 - マイグレーション: GORM(自動マイグレーション;本番環境では注意)、Ent(生成/差分SQL)、Bun(
bun/migrate)、sqlc(外部ツール)。 - フック/拡張性: GORM(コールバック/プラグイン)、Ent(フック/ミドルウェア + テンプレート/コード生成)、Bun(ミドルウェアのようなクエリフック、生SQLが簡単)、sqlc(アプリ層で組み立てる)。
- JSON/配列 (Postgres): BunとGORMには便利なヘルパーがあります;Ent/sqlcはカスタム型やSQLで処理します。
4つすべてに関する横断的な注意として:Go 1.27 では encoding/json がv2エンジンによりサポートされるようになりました。これは挙動を維持しつつ、一部のエラーメッセージを変更し、明示的なv2 API下ではnilスライスやマップキーの出力に影響を与えます。モデルがJSONをラウンドトリップさせる場合、アップグレード前にゴールデンテストのフィクスチャについて何を変えるべきかを確認するために、Go 1.27 アップグレードガイド をご覧ください。
何を選ぶべきか
- GORM を選ぶべき:最大限の利便性、豊富な機能、標準的なCRUDサービスにおける高速なプロトタイピングを望む場合。
- Ent を選ぶべき:コンパイル時の安全性、明示的なスキーマ、大規模チームでの長期的なメンテナンス性を重視する場合。
- Bun を選ぶべき:パフォーマンスと、ORMの利便性が役立つ場所ではORM風の、しかしSQL形状の明示的なクエリを望む場合。
- sqlc を選ぶべき:あなた(およびチーム)が、型安全なGoバインディングとゼロのランタイムオーバーヘッドを備えた純粋なSQLを好む場合。sqlc はまた、GoにおけるCQRSアーキテクチャ のリードモデル側にも自然に適しています。ここでは、クエリはドメインエンティティではなく呼び出し元に合わせて形作られ、明示的なSQLによりプロジェクションに対する完全な制御が可能になるからです。
このORMの選択と、統合スタイルやサービス境界の間でまだバランスを取っている場合は、このアプリアーキテクチャの概要 が、その決定をより広いプロダクション文脈に位置づけるのに役立ちます。
ローカルPostgreSQLのための最小限の docker-compose.yml
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