SQLC対GORM - Goにおけるデータベース対話の2つのアプローチ
Wenhao Wang
Dev Intern · Leapcell

はじめに
データベースとの対話は、ほぼすべての真剣なアプリケーションの基盤であり、Goはその強力な型付けとパフォーマンス特性により、このようなシステムの構築において人気のある選択肢となっています。Goでリレーショナルデータベースを扱う際、開発者はしばしば2つの広範なツールのカテゴリから選択することになります。1つは生のSQLとコード生成を重視するもので、もう1つはオブジェクト・リレーショナル・マッピング(ORM)レイヤーを提供するものです。この記事では、これらのカテゴリの代表的な2つの例、SQLCとGORMを掘り下げます。それぞれの思想、実践的な実装、および適切なユースケースを探り、Goにおけるデータベース対話という課題にそれぞれがどのようにアプローチしているかを明確に理解できるようにします。
思想の理解:SQLCとGORM
具体例に入る前に、関連するコアコンセプトについて共通の理解を確立しましょう。
SQLC(SQL Code Generation): SQLCは、生のSQLクエリからGoコードを生成するツールです。その思想は、SQLの宣言的な性質は強力であり、直接活用されるべきであるという信念に基づいています。SQLクエリを記述することで、SQLCはそれらのクエリを実行し、結果をGoの構造体にスキャンするための型安全なGoコードを自動生成します。このアプローチは、明示的なSQL、コンパイル時の安全性、およびデータベース操作に対する最小限の抽象化を優先します。
GORM(Go ORM - Object-Relational Mapping): GORMは、それとは対照的に、Go向けの本格的なORMライブラリです。その思想は、データベーステーブルをGoの構造体にマッピングし、生のSQLではなく、Goのイディオムを使用してデータベースと対話するための高レベルAPIを提供することに中心を置いています。GORMは、基盤となるSQLを抽象化し、開発者がGoオブジェクトとしてデータを操作できるようにすることを目指しています。このアプローチは、開発者の利便性、迅速な開発、およびデータベースのオブジェクト指向ビューを優先します。
これらの違いを実際的な例で示しましょう。
SQLC:明示的なSQLとコード生成を活用する
SQLCのワークフローでは、.sqlファイルにSQLスキーマとクエリを記述します。次にSQLCがこれらのファイルを処理してGoコードを生成します。
例:SQLCでのスキーマとクエリの定義
まず、SQLスキーマを定義します(例: schema.sql):
CREATE TABLE authors ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, bio TEXT );
次に、SQLクエリを定義します(例: query.sql):
-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1; -- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name; -- name: CreateAuthor :one INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio; -- name: UpdateAuthor :one UPDATE authors SET name = $1, bio = $2 WHERE id = $3 RETURNING id, name, bio; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1;
sqlc generateを実行した後、SQLCはGoファイル(例: db.go、models.go、query.sql.go)を生成します。以下は、生成されたquery.sql.goのスニペットです:
// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.25.0 // source: query.sql package db import ( "context" ) const createAuthor = `-- name: CreateAuthor :one INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio ` type CreateAuthorParams struct { Name string `json:"name"` Bio *string `json:"bio"` } func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) var i Author err := row.Scan( &i.ID, &i.Name, &i.Bio, ) return i, err } const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 ` func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { row := q.db.QueryRowContext(ctx, getAuthor, id) var i Author err := row.Scan( &i.ID, &i.Name, &i.Bio, ) return i, err } // ... other generated functions
そして、アプリケーションでの使用方法:
package main import ( "context" "database/sql" "fmt" _ "github.com/lib/pq" // PostgreSQL driver "your-project/db" // Assuming db package is where sqlc generates code ) func main() { connStr := "user=postgres password=password dbname=sqlc_example sslmode=disable" conn, err := sql.Open("postgres", connStr) if err != nil { panic(err) } defer conn.Close() queries := db.New(conn) ctx := context.Background() // Create an author newAuthor, err := queries.CreateAuthor(ctx, db.CreateAuthorParams{ Name: "Jane Doe", Bio: sql.NullString{String: "A talented writer", Valid: true}, }) if err != nil { panic(err) } fmt.Printf("Created author: %+v\n", newAuthor) // Get an author author, err := queries.GetAuthor(ctx, newAuthor.ID) if err != nil { panic(err) } fmt.Printf("Fetched author: %+v\n", author) // List authors authors, err := queries.ListAuthors(ctx) if err != nil { panic(err) } fmt.Printf("All authors: %+v\n", authors) }
SQLCの主な利点:
- コンパイル時の安全性: SQLのエラーは実行時ではなくコンパイル時に検出されます。
- ランタイムリフレクションなし: 生成されたコードはプレーンなGoであり、優れたパフォーマンスにつながります。
- 直接的なSQL制御: 複雑な結合、ウィンドウ関数など、SQLの全機能を利用できます。
- ボイラープレートの削減: 結果の構造体へのスキャンは自動的に処理されます。
しかし、追加のgenerateステップが必要であり、単純なCRUD操作では冗長に感じられることがあります。
GORM:Goのためのオブジェクト・リレーショナル・マッピング
GORMは異なるアプローチをとり、データベーステーブルに直接マッピングされるGo構造体を定義できるようにします。
例:GORMでのモデルと操作の定義
まず、Go構造体(モデル)を定義します:
package main import ( "gorm.io/gorm" ) type Author struct { gorm.Model // Provides ID, CreatedAt, UpdatedAt, DeletedAt Name string `gorm:"not null"` Bio *string }
次に、GORMのAPIを使用してデータベースと対話します:
package main import ( "fmt" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" // Import logger for better output "log" "os" "time" ) func main() { dsn := "host=localhost user=postgres password=password dbname=gorm_example port=5432 sslmode=disable TimeZone=Asia/Shanghai" // Configure GORM logger newLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer logger.Config{ SlowThreshold: time.Second, // Slow SQL threshold LogLevel: logger.Info, // Log level IgnoreRecordNotFoundError: false, // Ignore ErrRecordNotFound error for logger Colorful: true, // Disable color }, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: newLogger, // Apply the logger }) if err != nil { panic("failed to connect database") } // Migrate the schema (creates authors table if it doesn't exist) db.AutoMigrate(&Author{}) // Create an author bio := "A talented writer" author := Author{Name: "John Doe", Bio: &bio} result := db.Create(&author) // pass pointer of data to Create if result.Error != nil { panic(result.Error) } fmt.Printf("Created author: %+v\n", author) // Get an author by its primary key var fetchedAuthor Author db.First(&fetchedAuthor, author.ID) // find author with id 1 fmt.Printf("Fetched author: %+v\n", fetchedAuthor) // Update an author db.Model(&fetchedAuthor).Update("Name", "Jonathan Doe") fmt.Printf("Updated author: %+v\n", fetchedAuthor) // Delete an author (soft delete by default with gorm.Model) // db.Delete(&fetchedAuthor, fetchedAuthor.ID) // This would soft delete }
GORMは以下を提供します:
- 利便性と迅速性: SQLの抽象化により、一般的な操作のボイラープレートが削減されます。
- オブジェクト指向の対話: Go構造体と直接やり取りでき、Go開発者にとってよりイディオマティックなコードになります。
- 高度な機能: 組み込みのフック、関連付け、遅延読み込み、トランザクション管理。
- データベース抽象化: 最小限のコード変更で、さまざまなSQLデータベースを簡単に切り替えることができます。
しかし、GORMはランタイムリフレクションに依存しており、パフォーマンスのオーバーヘッド(ただし、一般的なアプリケーションではしばしば無視できる程度)をもたらす可能性があります。また、SQLを抽象化するため、複雑なクエリのデバッグが困難になる場合があり、データベース固有の機能へのアクセスが制限されることがあります。
どちらを選択するか
** SQLC を選択する場合:**
- SQL、パフォーマンス、コンパイル時の保証を完全に制御することを優先する場合。
- 複雑で手動で最適化された SQL クエリを含むプロジェクトの場合。
- ランタイムリフレクションとその潜在的なパフォーマンスへの影響を避けたい場合。
- 「SQLファースト」の開発アプローチを好む場合。
- 既存のデータベーススキーマに直接統合する必要がある場合。
** GORM を選択する場合:**
-
迅速な開発、利便性、およびより高いレベルの抽象化を優先する場合。
-
プロジェクトで主に標準的なCRUD操作を行う場合。
-
生のSQLではなく、Go構造体で作業することを好む場合。
-
関連付け、フック、マイグレーションなどの組み込み機能が必要な場合。
-
さまざまなSQLデータベースを切り替える必要がある可能性がある場合。
結論
SQLCとGORMは、Goでデータベースと対話するための2つの異なる、しかし同様に有効な思想を表しています。SQLCは、コード生成による明示的なSQLとコンパイル時の安全性を支持し、手動で調整されたクエリに深く依存するアプリケーションに対して、比類のない制御とパフォーマンスを提供します。一方、GORMは、オブジェクト・リレーショナル・マッピングの利便性と抽象化を採用し、そのイディオマティックなGo APIと堅牢な機能セットで開発を加速します。最終的に、両者の選択は、プロジェクトの要件、チームの好み、およびSQLの明示性とGoレベルの抽象化の間の望ましいバランスにかかっています。どちらのツールも、それぞれの領域で優れており、Goアプリケーションでのデータ管理のための強力な方法を提供します。