Contents
1. Go モジュールでのインストール
Go Modules を利用すれば依存関係が go.mod に自動で記録され、ビルド再現性が保証されます。過去の記事では固定バージョン(v1.25.0)を指定していましたが、常に最新リリースを取得したい場合は次の手順がおすすめです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# プロジェクトのルートでモジュール初期化(未実施の場合のみ) go mod init example.com/myapp # GORM 本体と PostgreSQL ドライバをインポートだけ書く cat <<EOF >> go.mod require ( gorm.io/gorm latest gorm.io/driver/postgres latest ) EOF # 依存関係を解決し、最新のタグへ自動的に合わせる go mod tidy |
latestは モジュールが解決できる最新版(GitHub のリリースページの最新タグ)を指します。go mod tidyは未使用の依存関係を削除し、必要なパッケージだけを取得するため、手動でバージョン番号を書く必要がなくなります。
参考: GORM のリリース一覧は https://github.com/go-gorm/gorm/releases で確認できます。
Go 1.17 以降の推奨インストール方法
Go 1.17 からは go get がビルド時に依存関係を取得する目的だけで使われ、バイナリインストール(CLI ツール等)には go install pkg@version が推奨されています。GORM はライブラリなので go get の代わりに上記の モジュール宣言+go mod tidy を利用してください。
2. バージョニングの誤解を防ぐ
GORM v2 は モジュールパスが gorm.io/gorm のままで、バージョン番号に v2 が付かない という特殊なバージョニング方式です。そのため、公式サイトの「Versioning」ページで説明されている通り、メジャーバージョンはリポジトリタグだけで管理されています。以下のリンクで詳細を確認できます。
- GORM バージョニング解説: https://gorm.io/docs/version.html
この情報を踏まえて、go.mod に記載されるバージョンは v1.xx.x(例:v1.25.4)ですが、実際には GORM v2 系 の機能が提供されています。
3. エラーハンドリングの統一方針
記事内では log.Fatalf と panic が混在していました。プロダクションコードでは エラーはロガーに委ねて呼び出し元へ返す、または コンテキストに紐付けた適切なログレベルで記録する ことがベストプラクティスです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// pkg/logger/logger.go package logger import ( "log" ) // Error はエラー発生時に標準ロガーへ出力し、エラーをそのまま返すユーティリティ func Error(err error, msg string) error { if err != nil { log.Printf("[ERROR] %s: %v", msg, err) } return err } |
上記ヘルパーを利用した例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import ( "gorm.io/driver/postgres" "gorm.io/gorm" "example.com/myapp/pkg/logger" ) func ConnectPostgres() (*gorm.DB, error) { dsn := "host=localhost user=myuser password=mypass dbname=mydb port=5432 sslmode=disable TimeZone=Asia/Tokyo" db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{}) if logger.Error(err, "PostgreSQL 接続失敗") != nil { return nil, err } return db, nil } |
log.Fatalfのようにプロセスを即終了させる 代わりに、エラーは呼び出し側でハンドリングできるように返します。- テストやリトライロジックが必要な場面でも同一のインターフェースで扱えるため、コード全体の可読性と保守性が向上します。
データベース接続設定
データベースごとの DSN(Data Source Name)の書式は異なりますが、gorm.Open(driver, &gorm.Config{}) で統一的に接続できます。以下では PostgreSQL / MySQL / SQLite の実装例を示し、先ほどのエラーハンドリングヘルパーを適用しています。
PostgreSQL
PostgreSQL は標準的なキーバリュー形式の DSN が推奨されます。環境変数で管理すると CI/CD との相性が良くなるため、コード例では os.Getenv を併用しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import ( "os" "gorm.io/driver/postgres" "gorm.io/gorm" "example.com/myapp/pkg/logger" ) func ConnectPostgres() (*gorm.DB, error) { dsn := os.Getenv("POSTGRES_DSN") if dsn == "" { // デフォルト開発用 DSN dsn = "host=localhost user=myuser password=mypass dbname=mydb port=5432 sslmode=disable TimeZone=Asia/Tokyo" } db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{}) if logger.Error(err, "PostgreSQL 接続失敗") != nil { return nil, err } return db, nil } |
sslmode=disableはローカル開発向けです。本番環境では必ず TLS を有効にしてください。- 環境変数 を利用することで、コードのハードコーディングを回避し、設定変更がデプロイなしで可能になります。
MySQL
MySQL の DSN は username:password@protocol(address)/dbname?param=value 形式です。文字エンコーディングやタイムゾーンに注意してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import ( "os" "gorm.io/driver/mysql" "gorm.io/gorm" "example.com/myapp/pkg/logger" ) func ConnectMySQL() (*gorm.DB, error) { dsn := os.Getenv("MYSQL_DSN") if dsn == "" { dsn = "myuser:mypass@tcp(localhost:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local" } db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if logger.Error(err, "MySQL 接続失敗") != nil { return nil, err } return db, nil } |
charset=utf8mb4は絵文字や多言語対応に必須です。parseTime=Trueとloc=Localがないと、time.Timeのマッピングが期待通りにならないことがあります。
SQLite(ファイル / メモリ)
SQLite は軽量でテスト向きです。ファイル保存かインメモリかを引数で切り替えられるようにラッパー関数を用意しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import ( "gorm.io/driver/sqlite" "gorm.io/gorm" "example.com/myapp/pkg/logger" ) func ConnectSQLite(path string) (*gorm.DB, error) { db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) if logger.Error(err, "SQLite 接続失敗") != nil { return nil, err } return db, nil } // メモリ上だけで完結させたいときのヘルパー func ConnectSQLiteMemory() (*gorm.DB, error) { return ConnectSQLite(":memory:") } |
- テストコードでは
ConnectSQLiteMemoryを利用すると、外部依存が無く高速に実行できます。
モデル定義と基本 CRUD 操作
GORM のモデルは 構造体タグ でテーブルスキーマを宣言します。以下の例では主要なタグ(primaryKey・size・uniqueIndex 等)を網羅し、JSON エンコード時の挙動も示しています。
構造体タグの書き方
|
1 2 3 4 5 6 7 8 9 |
type User struct { ID uint `gorm:"primaryKey;autoIncrement" json:"id"` Name string `gorm:"size:100;not null" json:"name"` Email string `gorm:"uniqueIndex;size:200" json:"email"` Age int `gorm:"default:0" json:"age,omitempty"` CreatedAt time.Time `json:"created_at"` // GORM が自動で更新 UpdatedAt time.Time `json:"updated_at"` } |
primaryKeyは自動的にインデックス化されます。sizeやnot nullはテーブル作成時にそのまま反映され、マイグレーションの手間が省けます。omitemptyにより JSON へシリアライズするときにゼロ値フィールドは除外されます。
統一された CRUD 実装例
以下では先ほど作成した logger.Error ヘルパーを用い、エラーはすべて呼び出し元へ返しています。これにより テストやリトライロジック が容易になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import ( "example.com/myapp/pkg/logger" "gorm.io/gorm" ) // Create func CreateUser(db *gorm.DB, u *User) error { return logger.Error(db.Create(u).Error, "ユーザー作成失敗") } // Read (First) func GetUserByEmail(db *gorm.DB, email string) (*User, error) { var user User if err := db.First(&user, "email = ?", email).Error; err != nil { return nil, logger.Error(err, "メールアドレス検索失敗") } return &user, nil } // Read (Find with 条件) func ListAdults(db *gorm.DB) ([]User, error) { var adults []User if err := db.Find(&adults, "age >= ?", 18).Error; err != nil { return nil, logger.Error(err, "成人ユーザー取得失敗") } return adults, nil } // Update (単一フィールド) func UpdateUserAge(db *gorm.DB, id uint, newAge int) error { return logger.Error( db.Model(&User{}).Where("id = ?", id).Update("age", newAge).Error, "年齢更新失敗", ) } // Delete func DeleteUser(db *gorm.DB, id uint) error { return logger.Error(db.Delete(&User{}, id).Error, "ユーザー削除失敗") } |
db.Model(&User{})を使うことで、インスタンスを生成せずに対象テーブルだけ指定できます。- すべての関数が
errorを返すため、サービス層やハンドラで一元的にエラーハンドリングが可能です。
クエリビルダーの活用
GORM のクエリビルダーは メソッドチェーン によって柔軟かつ安全に SQL を組み立てられます。以下ではページング、集計、特定カラム抽出を例示し、コード前に簡単な導入文を付けています。
ページングと並び替え
ユーザー一覧を年齢フィルタ付きで取得するケースです。Where のプレースホルダは SQL インジェクション対策になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// ListUsers は指定ページとページサイズで成年ユーザーを降順に取得します。 func ListUsers(db *gorm.DB, page, pageSize int) ([]User, error) { var users []User offset := (page - 1) * pageSize err := db. Where("age >= ?", 18). Order("created_at DESC"). Limit(pageSize). Offset(offset). Find(&users).Error return users, logger.Error(err, "ユーザー一覧取得失敗") } |
Orderに複数カラムを渡すことも可能です(例:"age ASC, name DESC")。
カラムだけ抽出する Pluck と集計クエリ
以下はメールアドレスのリスト取得と、年齢別ユーザー数の集計例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// GetEmails は全ユーザーのメールアドレスのみをスライスで返します。 func GetEmails(db *gorm.DB) ([]string, error) { var emails []string err := db.Model(&User{}).Pluck("email", &emails).Error return emails, logger.Error(err, "メール抽出失敗") } // CountByAgeGroup は年齢ごとのユーザー数を map で返します。 func CountByAgeGroup(db *gorm.DB) (map[int]int64, error) { type result struct { Age int Total int64 } var rows []result err := db.Model(&User{}). Select("age, COUNT(*) as total"). Group("age"). Scan(&rows).Error if logger.Error(err, "年齢別集計失敗") != nil { return nil, err } m := make(map[int]int64, len(rows)) for _, r := range rows { m[r.Age] = r.Total } return m, nil } |
Pluckは余分な構造体を定義せずに単一列だけ取得でき、コードがすっきりします。- 集計結果は
Scanで好きな構造体へマッピング可能です。
アソシエーションとフック
リレーションやライフサイクルフックはモデル層の責務を高めます。以下では Has One / Has Many, Belongs To / Many‑to‑Many の宣言方法、Preload による N+1 問題回避、そしてパスワードハッシュ化やキャッシュ削除といったフック実装例を示します。
リレーションの定義
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type Profile struct { ID uint `gorm:"primaryKey"` UserID uint `gorm:"uniqueIndex"` // Has One の外部キー Bio string } type Post struct { ID uint `gorm:"primaryKey"` UserID uint // Has Many の外部キー Title string Body string } |
User 側にリレーションフィールドを追加すると、GORM が自動で結合情報を認識します。
|
1 2 3 4 5 6 7 8 9 10 |
type User struct { ID uint `gorm:"primaryKey"` Name string Email string `gorm:"uniqueIndex"` Profile Profile // Has One Posts []Post // Has Many Roles []Role `gorm:"many2many:user_roles;"` RoleID uint // Belongs To 用外部キー(任意) } |
- Has One は
uniqueIndexが付いた外部キーで 1 対 1 の関係を表します。 - Has Many はスライス型フィールドで 1 対 多の関係を示し、
UserIDが自動的に外部キーになります。 - Many‑to‑Many は
many2many:テーブル名;タグで結合テーブル名を明示します。
Preload による事前ロード
|
1 2 3 4 5 6 7 8 9 10 11 |
// GetUserWithRelations は Profile, Posts, Role を同時に取得し、N+1 問題を防ぎます。 func GetUserWithRelations(db *gorm.DB, id uint) (*User, error) { var user User err := db. Preload("Profile"). Preload("Posts", "title LIKE ?", "%GORM%"). Preload("Roles"). First(&user, id).Error return &user, logger.Error(err, "ユーザー取得失敗") } |
Preloadの第2引数は条件付きロードが可能です。上記例ではタイトルに「GORM」を含む投稿だけを事前取得しています。
ライフサイクルフックの実装例
以下はパスワードハッシュ化とキャッシュ削除という、副作用処理 をモデル側で完結させる典型的なパターンです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import ( "context" "fmt" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type User struct { ID uint `gorm:"primaryKey"` Name string Email string `gorm:"uniqueIndex"` Password string // 保存時はハッシュ化された文字列になる } // BeforeCreate はレコード作成直前に呼び出され、パスワードをハッシュ化します。 func (u *User) BeforeCreate(tx *gorm.DB) (err error) { if u.Password == "" { return fmt.Errorf("password is required") } hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) if err != nil { return err } u.Password = string(hashed) return nil } // AfterUpdate は更新後にキャッシュを削除します(例: Redis)。 func (u *User) AfterUpdate(tx *gorm.DB) error { cacheKey := fmt.Sprintf("user:%d", u.ID) // redisClient は別ファイルで初期化済みと仮定 if err := redisClient.Del(context.Background(), cacheKey).Err(); err != nil { return fmt.Errorf("cache delete failed: %w", err) } return nil } |
- フックは
*gorm.DBを受け取るので、トランザクション内での追加処理が容易です。 - エラーはフック自体から返すことで、GORM がロールバックやエラーハンドリングを自動で行います。
マイグレーション・ロギング・テスト
開発フェーズと本番環境ではマイグレーションの扱い方が異なります。ここでは AutoMigrate の利用シーン、ロガー設定、そして SQLite in‑memory を使った単体テスト のベストプラクティスを紹介します。
AutoMigrate と本番マイグレーション
|
1 2 3 4 5 6 7 8 |
// Migrate は開発環境向けに自動でテーブル構造を同期させます。 func Migrate(db *gorm.DB) error { if err := db.AutoMigrate(&User{}, &Profile{}, &Post{}, &Role{}); err != nil { return logger.Error(err, "AutoMigrate 失敗") } return nil } |
- 推奨フロー
- ローカル・ステージング環境で
AutoMigrateを実行し、スキーマの変化を確認。 - 本番リリース時は
golang-migrate/migrate等の外部マイグレーションツールで SQL ファイル を管理し、手動または CI に組み込んで適用する。 - カラム削除や型変更はデータロスリスクが高いため、必ずバックアップと検証を行う。
ロガー設定とデバッグモード
GORM のロガーは標準ロガーだけでなく、zap, logrus など任意のロギングライブラリに差し替え可能です。以下は標準ロガーでスロークエリを可視化する例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import ( "log" "os" "time" "gorm.io/gorm" "gorm.io/gorm/logger" ) func SetupGORMLogger(db *gorm.DB) *gorm.DB { newLog := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), // io.Writer logger.Config{ SlowThreshold: time.Millisecond * 200, // スロークエリ閾値 LogLevel: logger.Info, // 開発は Info、運用は Warn/ Error 推奨 IgnoreRecordNotFoundError: false, Colorful: true, }, ) return db.Session(&gorm.Session{Logger: newLog}) } |
- 本番 では
LogLevel: logger.Warnまたはlogger.Errorに設定し、ログ量を抑えると同時に重要な警告だけが出力されます。 SlowThresholdを適切に調整することでパフォーマンスボトルネックの早期発見が可能です。
SQLite in‑memory を使ったユニットテスト
外部データベースに依存しないテストは CI の安定性を高めます。以下は testing パッケージだけで完結するサンプルです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import ( "testing" "example.com/myapp/pkg/logger" ) func TestUserCRUD(t *testing.T) { db, err := ConnectSQLiteMemory() if err != nil { t.Fatalf("テスト用 DB 接続失敗: %v", err) } // スキーマ自動作成 if err = Migrate(db); err != nil { t.Fatalf("マイグレーション失敗: %v", err) } // CREATE user := User{Name: "Bob", Email: "bob@example.com", Password: "plain"} if err = CreateUser(db, &user); err != nil { t.Fatalf("INSERT 失敗: %v", err) } // READ fetched, err := GetUserByEmail(db, "bob@example.com") if err != nil { t.Fatalf("SELECT 失敗: %v", err) } if fetched.Name != "Bob" { t.Errorf("取得した名前が期待と異なる: got=%s want=Bob", fetched.Name) } // UPDATE if err = UpdateUserAge(db, fetched.ID, 31); err != nil { t.Fatalf("UPDATE 失敗: %v", err) } // DELETE if err = DeleteUser(db, fetched.ID); err != nil { t.Fatalf("DELETE 失敗: %v", err) } } |
- テストは 状態を持たない SQLite メモリ DB 上で実行され、テストケースごとにクリーンな環境が保証されます。
logger.Errorを使用しているため、エラーログが標準出力へ流れ、デバッグが容易です。
まとめ
- インストールはモジュール宣言+
go mod tidyで最新版を取得し、バージョン固定による陳腐化リスクを回避します。 - バージョニングは GORM v2 が
gorm.io/gormに統合されている点に注意し、公式のバージョニングページへのリンクを添えて誤解防止を行いました。 - エラーハンドリングは ロガーで記録しエラーを返す 形に統一し、
panicやlog.Fatalfの混在を排除しました。 - 各データベース(PostgreSQL・MySQL・SQLite)の接続コードは環境変数活用とヘルパー関数で簡潔化し、テスト容易性を向上させました。
- モデル定義・CRUD・クエリビルダーのサンプルは 統一されたロガーヘルパー を使用し、実務に即した形に整理しました。
- アソシエーションとフックの実装例で リレーション宣言 と 副作用処理(ハッシュ化・キャッシュ削除)を示し、モデル層の責務を明確化しました。
- 開発時は
AutoMigrate、本番は外部マイグレーションツールと組み合わせるベストプラクティス、ロガー設定のチューニングポイント、そして SQLite in‑memory テスト の具体例を提供しました。
これらの手順とコードパターンをプロジェクトに取り入れれば、GORM を安全・効率的に運用できるだけでなく、将来的なバージョンアップやテスト自動化にも柔軟に対応できます。ぜひ実際にコードを書きながら試してみてください。