PostgreSQL用Go ORMの比較:GORM、Ent、Bun、sqlc

GOにおけるORMの実践的かつコード中心の解説

目次

最も注目すべき Go用ORMは、GORM、Ent、Bun、sqlcです。 ここでは、それらを比較し、純粋なGoでのCRUD操作の例を示します。

golang + postgresql

TL;DR

  • GORM: 機能豊富で便利。「とりあえずリリース」するには最も簡単ですが、ランタイムのオーバーヘッドが比較的多いです。
  • Ent: スキーマ即コードのアプローチで、型安全なAPIを生成します。大規模なコードベースやリファクタリングには最適です。
  • Bun: 軽量でSQLファーストなクエリビルダー/ORM。PostgreSQLの機能を活かしやすく、設計上は明示的です。
  • sqlc (厳密にはORMではありませんが): SQLを書いて型安全なGoコードを得ます。最高のパフォーマンスと制御性を持ち、ランタイムの「魔法」はありません。

選択基準と簡易比較

私の評価基準は以下の通りです。

  • パフォーマンス: レイテンシ/スループット、避けられるオーバーヘッド、バッチ操作。
  • DX (開発者体験): 学習曲線、型安全性、デバッグのしやすさ、コード生成の摩擦。
  • エコシステム: ドキュメント、例、アクティビティ、統合(マイグレーション、トレーシング)。
  • 機能セット: リレーション、イーグルローディング(早期読み込み)、マイグレーション、フック、生SQLのエスケープハッチ。
ツール Paradigm(パラダイム) 型安全性 リレーション マイグレーション 生SQLの使いやすさ 典型的なユースケース
GORM Active RecordスタイルのORM 中程度(ランタイム) Yes(タグ、Preload/Joins) Auto-migrate(オプトイン) db.Raw(...) 迅速なデリバリー、豊富な機能、従来型のCRUDアプリ
Ent スキーマ → コード生成 → フルーエントAPI 高(コンパイル時) 一等市民(edges) 生成されたSQL(別ステップ) entsql, カスタムSQL 大規模コードベース、リファクタリングが多いチーム、厳格なタイピング
Bun SQLファーストなクエリビルダー/ORM 中〜高 明示的(Relation 別のmigrateパッケージ 自然(ビルダー + 生SQL) パフォーマンス重視のサービス、PostgreSQLの機能活用
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{})
}

// Auto-migrate (オプション; 本番環境では注意)
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
}

備考

  • .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)

  // 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、インデックス、カバリングインデックス、不要なラウンドトリップを避ける。
  • 接続を再利用し、ワークロードに基づいてプールサイズを調整する。

開発者体験とエコシステム

  • GORM: 最大のコミュニティ、多数の例/プラグイン; 高度なパターンには学習曲線が急峻。
  • Ent: 優れたドキュメント; コード生成ステップが主なメンタルモデルのシフト; リファクタリングに非常に優しい。
  • Bun: 読みやすく予測可能なクエリ; コミュニティは小さいが活発; PostgreSQLの利便性に優れている。
  • sqlc: ランタイム依存関係が最小; マイグレーションツールやCIとよく統合; SQLに慣れたチームには最適。

機能ハイライト

  • リレーションとイーグルローディング: すべてリレーションを処理; GORM (タグ + Preload/Joins)、Ent (edges + .With...())、Bun (Relation(...))、sqlc (自分でジョインを書く)。
  • マイグレーション: GORM (auto-migrate; 本番環境では注意)、Ent (生成/差分SQL)、Bun (bun/migrate)、sqlc (外部ツール)。
  • フック/拡張性: GORM (コールバック/プラグイン)、Ent (フック/ミドルウェア + テンプレート/コード生成)、Bun (ミドルウェアのようなクエリフック、簡単な生SQL)、sqlc (アプリレイヤーで合成)。
  • JSON/配列 (PostgreSQL): BunとGORMには便利なヘルパーあり; Ent/sqlcはカスタムタイプまたはSQLで処理。

何をいつ選ぶべきか

  • GORM を選ぶ理由: 最大限の利便性、豊富な機能、従来型のCRUDサービスのための迅速なプロトタイピングを望む場合。
  • Ent を選ぶ理由: コンパイル時の安全性、明示的なスキーマ、大規模チームでの長期的な保守性を重視する場合。
  • Bun を選ぶ理由: パフォーマンスと、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

GoにおけるORMパッケージとライブラリ

他の有用なリンク

購読する

システム、インフラ、AIエンジニアリングの新記事をお届けします。