PostgreSQL用のGo ORMの比較:GORM vs Ent vs Bun vs sqlc
GOにおけるORMの実用的な、コード中心の考察
目次
Go言語用の最も顕著なORMはGORM、Ent、Bun、sqlcです。 ここでは、純粋なGoでのCRUD操作の例とともに、それらの比較を行います。
TL;DR
- GORM: 機能が豊富で使いやすい; すぐにリリースできるが、実行時のオーバーヘッドが多い。
- Ent: スキーマからコード生成された、型安全なAPI; 大規模なコードベースやリファクタリングに最適。
- Bun: 軽量でSQLファーストのクエリビルダ/ORM; Postgresの機能が豊富で、設計上明確。
- sqlc (ORMではないが): SQLを書くと、型安全なGoが生成される; 最も高いパフォーマンスと制御、実行時の魔法なし。
選定基準と簡単な比較
私の基準は以下の通りです:
- パフォーマンス: ラテンシー/スループット、避けられるオーバーヘッド、バッチ操作。
- DX: 学習曲線、型安全性、デバッグ可能性、コード生成の摩擦。
- エコシステム: ドキュメント、例、活動性、統合(マイグレーション、トレース)。
- 機能セット: 関係、急いで読み込み、マイグレーション、フック、生SQLエスケープハッチ。
ツール | パラダイム | 型安全性 | 関係 | マイグレーション | 生SQLの使いやすさ | 一般的な使用ケース |
---|---|---|---|---|---|---|
GORM | Active Record–style ORM | 中(実行時) | はい(タグ、Preload/Joins) | 自動マイグレート(オプション) | db.Raw(...) |
高速な配信、豊富な機能、従来のCRUDアプリ |
Ent | スキーマ → コード生成 → 流暢なAPI | 高(コンパイル時) | 一等(エッジ) | 生成されたSQL(別のステップ) | entsql , カスタムSQL |
大規模なコードベース、リファクタリングが頻繁なチーム、厳格な型付け |
Bun | SQLファーストのクエリビルダ/ORM | 中–高 | 明示的(Relation ) |
別のマイグレーションパッケージ | 自然(ビルダ+生SQL) | パフォーマンスを重視するサービス、Postgresの機能 |
sqlc | SQL → コード生成関数(ORMではない) | 高(コンパイル時) | SQLジョイン経由 | 外部ツール(例: golang-migrate) | それはSQLです | 最大の制御と速度; DBAチームに最適 |
例によるCRUD
設定(PostgreSQL)
pgxまたはツールのネイティブPGドライバを使用してください。例のDSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
インポート(すべてのORMに共通)
goコードの例のファイルの先頭に追加してください:
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 {
// 作成
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// 読み取り
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// 更新
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// 削除
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 {
// 作成
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// 読み取り
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// 更新
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// 削除
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
注意点
- 終端から端まで強力な型付け; 関係のためのエッジ。
- 生成されたマイグレーションまたはお好みのマイグレーションツールを使用してください。
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 {
// 作成
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// 読み取り
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// 更新
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// 削除
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
注意点
.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" # または "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)
// 作成
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// 読み取り
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// 更新
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// 削除
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
、インデックス、カバリングインデックス、不要なラウンドトリップを避けてください。 - 接続を再利用し、ワークロードに基づいてプールサイズを調整してください。
開発者体験とエコシステム
- GORM: 最も大きなコミュニティ、多くの例/プラグイン; 高度なパターンでは学習曲線が急です。
- Ent: すばらしいドキュメント; コード生成ステップが主なメンタルモデルのシフト; リファクタリングが頻繁なチームにとって非常に優れています。
- Bun: 読みやすく予測可能なクエリ; 小さなが活発なコミュニティ; Postgresの便利な機能。
- sqlc: 最小限の実行時依存; マイグレーションツールやCIとスムーズに統合; SQLに精通したチームにとって最適です。
機能のハイライト
- 関係と急いで読み込み: すべてが関係を処理; GORM(タグ +
Preload
/Joins
)、Ent(エッジ +.With...()
)、Bun(Relation(...)
)、sqlc(あなたが結合を書きます)。 - マイグレーション: GORM(自動マイグレーション; 本番環境では注意)、Ent(生成された/差分SQL)、Bun(
bun/migrate
)、sqlc(外部ツール)。 - フック/拡張性: GORM(コールバック/プラグイン)、Ent(フック/ミドルウェア + テンプレート/コード生成)、Bun(ミドルウェアのようなクエリフック、生SQLが簡単)、sqlc(アプリ層で組み立て)。
- JSON/配列(Postgres): BunとGORMには便利なヘルパーがあります; Ent/sqlcはカスタムタイプまたはSQLで処理します。
何を選択するか
- GORMを選択してください。最大の便利さ、豊富な機能、従来のCRUDサービスの高速なプロトタイピングを望む場合。
- Entを選択してください。コンパイル時安全性、明示的なスキーマ、長期的な保守性を重視し、大規模なチームで作業する場合。
- Bunを選択してください。パフォーマンスと明示的なSQL型のクエリにORMの利便性を望む場合。
- sqlcを選択してください。チームがSQLと型安全なGoバインディングを好む場合、実行時のオーバーヘッドがゼロの場合。
ローカル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