🏃

【Go】GORM v1.30 ジェネリクスAPIで型安全にRaw SQL

に公開

はじめに

前回は、GORM v1.30で追加されたジェネリクスAPIで型安全にJoins()Preload()を使う方法を見ていきました。

今回は、同じくジェネリクスAPIを使って、型安全にRaw SQLを使う方法を見ていきます!

この記事でわかること

  • ジェネリクスAPIを使ったRawメソッドの使い方
  • ジェネリクスAPIを使ったExecメソッドの使い方

前提

ここでは、ユーザーを管理するusersテーブルが存在し、以下のようなデータが登録されているものとします。

-- SELECT * FROM users;
 id |  name  |  birthday  
----+--------+------------
  1 | tomoya | 2025-10-02
  2 | Alice  | 2025-10-03
  3 | Bob    | 2025-10-04
  4 | Cindy  | 2025-10-05

ジェネリクスAPIを用いたRaw SQLの使い方

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

1. Raw

GORMにおけるRawは、SELECT文などのデータ取得に使用され、クエリ結果を取得することができます。
従来のRaw SQLによる抽出は、以下のように実装することができます。

main.go
// 抽出に関わる部分のみ抜粋
type Result struct {
    ID       int       `gorm:"primaryKey"`
    Name     string    `gorm:"not null"`
    Birthday time.Time `gorm:"type:date"`
}

// ID=1のユーザーを抽出する
var result Result
db.Debug().Raw("SELECT id, name, birthday FROM users WHERE id = ?", 1).Scan(&result)
fmt.Printf("ID:%d, Name:%s, Birthday:%s\n", result.ID, result.Name, result.Birthday)

// テーブルに存在するレコード件数を抽出
var count int
db.Debug().Raw("SELECT COUNT(*) FROM users").Scan(&count)
fmt.Printf("Count: %d\n", count)

// UPDATEで変更したレコードのID・Name・Birthdayを返却
var results []Result
db.Debug().Raw("Update users SET birthday = ? RETURNING id, name, birthday", "2025-10-02").Scan(&results)
for _, r := range results {
    fmt.Printf("ID:%d, Name:%s, Birthday:%s\n", r.ID, r.Name, r.Birthday)
}

実行すると、以下のような出力結果となります。

ターミナル
# ID=1のユーザーを抽出する
[0.595ms] [rows:1] SELECT id, name, birthday FROM users WHERE id = 1
ID:1, Name:tomoya, Birthday:2025-10-02 00:00:00 +0000 UTC

# テーブルに存在するレコード件数を抽出
[0.400ms] [rows:1] SELECT COUNT(*) FROM users
Count: 4

# UPDATEで変更したレコードのID・Name・Birthdayを返却
[3.300ms] [rows:4] Update users SET birthday = '2025-10-02' RETURNING id, name, birthday
ID:1, Name:tomoya, Birthday:2025-10-02 00:00:00 +0000 UTC
ID:2, Name:Alice, Birthday:2025-10-02 00:00:00 +0000 UTC
ID:3, Name:Bob, Birthday:2025-10-02 00:00:00 +0000 UTC
ID:4, Name:Cindy, Birthday:2025-10-02 00:00:00 +0000 UTC

一方、ジェネリクスAPIを使うと、以下のように実装することができます。

main.go
type Result struct {
	ID       int       `gorm:"primaryKey"`
	Name     string    `gorm:"not null"`
	Birthday time.Time `gorm:"type:date"`
}

ctx := context.Background()

// ID=1のユーザーを抽出する
var result Result
err = gorm.G[Result](db.Debug()).Raw("SELECT id, name, birthday FROM users WHERE id = ?", 1).Scan(ctx, &result)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("ID:%d, Name:%s, Birthday:%s\n", result.ID, result.Name, result.Birthday)

// テーブルに存在するレコード件数を抽出
var count int
err = gorm.G[int](db).Raw("SELECT COUNT(*) FROM users").Scan(ctx, &count)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Count: %d\n", count)

// UPDATEで変更したレコードのID・Name・Birthdayを返却
var results []Result
err = gorm.G[Result](db.Debug()).Raw("Update users SET birthday = ? RETURNING id, name, birthday", "2025-10-02").Scan(ctx, &results)
if err != nil {
    log.Fatal(err)
}
for _, r := range results {
    fmt.Printf("ID:%d, Name:%s, Birthday:%s\n", r.ID, r.Name, r.Birthday)
}

実行すると、以下のような出力結果となります。

ターミナル
# ID=1のユーザーを抽出する
[0.857ms] [rows:1] SELECT id, name, birthday FROM users WHERE id = 1
ID:1, Name:tomoya, Birthday:2025-10-02 00:00:00 +0000 UTC

# テーブルに存在するレコード件数を抽出
[1.729ms] [rows:1] SELECT COUNT(*) FROM users
Count: 4

# UPDATEで変更したレコードのID・Name・Birthdayを返却
[3.256ms] [rows:4] Update users SET birthday = '2025-10-02' RETURNING id, name, birthday
ID:1, Name:tomoya, Birthday:2025-10-02 00:00:00 +0000 UTC
ID:2, Name:Alice, Birthday:2025-10-02 00:00:00 +0000 UTC
ID:3, Name:Bob, Birthday:2025-10-02 00:00:00 +0000 UTC
ID:4, Name:Cindy, Birthday:2025-10-02 00:00:00 +0000 UTC

2. Exec

GORMにおけるExecは、INSERT、UPDATE、DELETE文などのデータ変更に使用され、変更した行数やエラー情報を取得することができます。
今回は、UPDATEに焦点を絞って見ていきたいと思います。

従来のRaw SQLにおける更新は、以下のように実装することができます。

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

// ID=1のレコードのbirthdayを「2025-10-01」に更新
db.Debug().Exec("UPDATE users SET birthday = ? WHERE id = ?", "2025-10-01", 1)

// ID=1のレコードのbirthdayを、現在登録されている日に3日加算して更新
db.Debug().Exec("UPDATE users SET birthday = ? WHERE id = ?", gorm.Expr("birthday + INTERVAL '3 days'"), 1)

gorm.Expr()は、SQL式を埋め込むための仕組みです。
カウンターの増減や、SQL関数を使いたいとき(NOW()CONCAT() etc...)などに用いられます。

実行すると、以下のような出力結果となります。

ターミナル
# ID=1のレコードのbirthdayを「2025-10-01」に更新
[1.802ms] [rows:1] UPDATE users SET birthday = '2025-10-01' WHERE id = 1

# ID=1のレコードのbirthdayを、現在登録されている日に3日加算して更新
[0.862ms] [rows:1] UPDATE users SET birthday = birthday + INTERVAL '3 days' WHERE id = 1

一方、ジェネリクスAPIを使うと、以下のように実装することができます。

main.go
ctx := context.Background()

// ID=1のレコードのbirthdayを「2025-10-01」に更新
err = gorm.G[any](db.Debug()).Exec(ctx, "UPDATE users SET birthday = ? WHERE id = ?", "2025-10-01", 1)
if err != nil {
    log.Fatal(err)
}

// ID=1のレコードのbirthdayを、現在登録されている日に3日加算して更新
err = gorm.G[any](db.Debug()).Exec(ctx, "UPDATE users SET birthday = ? WHERE id = ?", gorm.Expr("birthday + INTERVAL '3 days'"), 1)
if err != nil {
    log.Fatal(err)
}

実行すると、以下のような出力結果となります。

ターミナル
# ID=1のレコードのbirthdayを「2025-10-01」に更新
[0.382ms] [rows:1] UPDATE users SET birthday = '2025-10-01' WHERE id = 1

# ID=1のレコードのbirthdayを、現在登録されている日に3日加算して更新
[0.400ms] [rows:1] UPDATE users SET birthday = birthday + INTERVAL '3 days' WHERE id = 1

このように実装、実行することができます。
しかし、ここでは型パラメータがanyになっているので、型安全性の恩恵を得ることができていないので、従来の実装方法でもいいような気がします。

まとめ

今回は、GORM v1.30で追加されたジェネリクスAPIを使って、型安全にRaw SQLを使う方法を見ていきました。

  • Raw
    • gorm.G[T] と型パラメータを明示できるため、コンパイル時に型チェックが効く
    • int のようなプリミティブ型や構造体に対しても安全にマッピング可能
  • Exec
    • データの登録・更新・削除に使用でき、gorm.Expr()を組み合わせることでSQL式も安全に扱える
    • ただし、Exec()の場合は戻り値が行数やエラーのみであり、型安全性の恩恵は小さく、従来の実装と大きな差はない

次回は、「ジェネリクスAPIを使ったトランザクション」について見ていきます!

参考

Discussion