🏃‍♂️

【Go】GORM v1.30 ジェネリクスAPIで型安全にレコードを登録する方法

に公開

はじめに

2025年5月25日に、Gorm v1.30.0 がリリースされました!🎉
(2025年9月7日時点では、v1.30.3が最新版になっていました!)

このバージョンアップにより、ジェネリクスAPIが利用可能になり、より型安全な開発が実現できるようになりました。
今回は、この新しいジェネリクスAPIを使ったレコードの登録方法について、従来の方法と比較しながら解説します!

この記事でわかること

  • ジェネリクスAPIとは
  • ジェネリクスAPIを使った単一レコードの登録
  • ジェネリクスAPIを使った複数レコードの登録

ジェネリクスAPIについて

Gormの公式ドキュメントでは、ジェネリクスAPIを次のように紹介しています。

GORM has officially introduced support for Go Generics in its latest version (>= v1.30.0). This addition significantly enhances usability and type safety while reducing issues such as SQL pollution caused by reusing gorm.DB instances.

要約すると、主なメリットは以下の2点です。

  • ジェネリクスAPIの登場で使いやすさと型安全性が大幅に向上
  • gorm.DB インスタンスの再利用によるSQL汚染の軽減

ジェネリクスAPIの導入により、開発者はより直感的かつ型安全なコードを記述できるようになりました。

また、従来は *gorm.DB インスタンスを再利用する際、以前のクエリ条件(例: Where句)が意図せず引き継がれてしまう「SQL汚染」という問題が発生することがありました。ジェネリクスAPIを利用することで、こうした意図しない副作用を大幅に減らせます。
この点については、以下の公式ドキュメントにも詳しく記載されています。

ジェネリクスAPIを使ったレコードの登録

ここでは、ジェネリクスAPIを使ったレコード登録の方法を、従来の実装と比較しながら見ていきましょう。

1. 単一レコードの登録

従来のレコードの登録は、以下のように実装することができます。

main.go
package main

import (
    "fmt"
    "log"
    "os"
    "time"
    
    "github.com/joho/godotenv"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    ID       uint      `gorm:"primaryKey"`
    Name     string    `gorm:"not null"`
    Birthday time.Time `gorm:"type:date"`
}

func connectDatabase() (*gorm.DB, error) {
    if err := godotenv.Load(); err != nil {
        log.Fatal(err)
    }
    
    dsn := fmt.Sprintf(
        "postgres://%s:%s@%s:%s/%s",
        os.Getenv("POSTGRES_USER"),
        os.Getenv("POSTGRES_PASSWORD"),
        os.Getenv("POSTGRES_HOST"),
        os.Getenv("POSTGRES_PORT"),
        os.Getenv("POSTGRES_DB"),
    )
    
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, fmt.Errorf("DB接続エラー:%s", err.Error())
    }
    
    db.AutoMigrate(User{})
    return db, nil
}

func main() {
    db, err := connectDatabase()
    if err != nil {
        log.Fatal(err)
    }
    
    birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
    user := User{Name: "Tomoya", Birthday: birthday}
    
    result := db.Create(&user)
    if result.Error != nil {
        log.Fatal(err)
    }
    
    fmt.Println("RowsAffected:", result.RowsAffected) // 出力 → RowsAffected: 1
    fmt.Println("Error:", result.Error)               // 出力 → Error: <nil>
}

Gormでレコードを登録するには、Createメソッドの引数に登録したいデータのポインタを渡すことでレコードを作成することができます。

しかし、同じような構造体を複数定義していて(例: UserAdmin)、誤って違うモデルを使ってしまった場合、以下のような間違いがコンパイル時に検知できません。

main.go
// 登録に関わる部分を抜粋
birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
user := Admin{Name: "Tomoya", Birthday: birthday}

result := db.Create(&user)
if result.Error != nil {
    log.Fatal(err)
}

本来はUserモデルで登録したかったにもかかわらず、誤ってAdminモデルのデータを渡してしまうといったヒューマンエラーが起こり得ます。

ジェネリクスAPIは、こうした問題を防ぐのに役立ちます。

main.go
// 登録に関わる部分を抜粋
birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
user := User{Name: "Tomoya", Birthday: birthday}
ctx := context.Background()

if err := gorm.G[User](db).Create(ctx, &user); err != nil {
    log.Fatal(err)
}

ジェネリクスAPIでレコードを登録するには、2つの関数・メソッドを使います。

  1. func G[T any](db *DB, opts ...clause.Expression) Interface[T]
  • 役割: 型パラメータ T を指定することで、その型に特化した *gorm.Gorm[T] を生成
  • メリット: 型安全な操作が可能
  1. func (c createG[T]) Create(ctx context.Context, r *T) error
  • 役割: 型 T のレコードを登録
  • 従来との違い: 返り値は *gorm.DB ではなく error

型パラメータを指定することで、別の型に関するレコードの登録を防ぐことができます。

誤ったレコードの登録をしていることをVSCode上で通知

また、以下のように実装することもできます。

main.go
// 登録に関わる部分を抜粋
birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
user := User{Name: "Tomoya", Birthday: birthday}
ctx := context.Background()

result := gorm.WithResult()
if err := gorm.G[User](db, result).Create(ctx, &user); err != nil {
    log.Fatal(err)
}
fmt.Println("RowAffected:", result.RowsAffected) // 出力 → RowsAffected: 1

こちらは従来のレコード登録方法と似ている感じがします。

2. 複数レコードの登録

従来は、単一レコードを登録するとき同様、Createメソッドを使えば複数のレコードを登録することができました。
これは、Createメソッドの引数が空のinterface型でどんな型でも指定することができたからです。

finisher_api.go
func (db *DB) Create(value interface{}) (tx *DB)

この場合、構造体のスライスを Createメソッド の引数渡すことで複数レコードを登録することができます。

一方、ジェネリクスAPIの Createメソッド は以下のようなシグネチャで、ポインタ型*Tしか受け付けていないのでスライスを渡すことができません。

generics.go
func (c createG[T]) Create(ctx context.Context, r *T) error

なので、複数レコードを登録するには、CreateInBatchesメソッドを使います。

main.go
func (c createG[T]) CreateInBatches(ctx context.Context, r *[]T, batchSize int) error

バッチサイズを指定することで複数のレコードを登録することができます。
例えば、以下のように使うことができます。

main.go
// 登録に関わる部分を抜粋
birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
users := []User{
    {Name: "Tomoya", Birthday: birthday},
    {Name: "Alice", Birthday: birthday},
    {Name: "Bob", Birthday: birthday},
    {Name: "Tom", Birthday: birthday},
}
ctx := context.Background()

// 4件のデータをバッチサイズ2で登録
if err := gorm.G[User](db).CreateInBatches(ctx, &users, 2); err != nil {
    log.Fatal(err)
}

この場合、4件のレコードを2回に分けて登録するようにしています。
実際に登録処理を走らせると、以下のようなSQL文が実行されたことがわかります。

ターミナル
[1.220ms] [rows:2] INSERT INTO "users" ("name","birthday") VALUES ('Tomoya','2025-09-03 00:00:00'),('Alice','2025-09-03 00:00:00') RETURNING "id"

[0.281ms] [rows:2] INSERT INTO "users" ("name","birthday") VALUES ('Bob','2025-09-03 00:00:00'),('Tom','2025-09-03 00:00:00') RETURNING "id"

まとめ

今回は、GORM v1.30で追加されたジェネリクスAPIを使って、型安全にレコード登録する方法を見ていきました。

  • 型安全性が向上し、コンパイル時に型エラーを検出できる
  • gorm.DB インスタンスの再利用による SQL 汚染を軽減
  • Createメソッド を使うことで型安全に単一レコードを登録できる
  • CreateInBatches を使うことで型安全に複数レコードを登録できる

次回は、「ジェネリクスAPIを使って型安全にレコードを抽出する方法」について見ていきます!

参考

Discussion