🏃‍♂️

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

に公開

はじめに

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

今回は、同じくジェネリクスAPIを使って、型安全にレコードを更新する方法を見ていきます!

この記事でわかること

  • 単一カラムの更新方法
  • 複数カラムの更新方法

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

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

1. 単一カラムの更新

従来の単一カラムの更新は、以下のように実装することができます。

main.go
// 更新に関わる部分のみ抽出
type User struct {
    ID        uint      `gorm:"primaryKey"`
    Name      string    `gorm:"not null"`
    Birthday  time.Time `gorm:"type:date"`
    UpdatedAt time.Time `gorm:"type:date"`
}

birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
user := User{ID: 1, Name: "Tomoya", Birthday: birthday}

// 1. 構造体リテラルをModelメソッドに渡し、条件を設定
db.Model(&User{}).Debug().Where("id = ?", 1).Update("name", "tomoya")

// 2. 構造体インスタンスをModelメソッドに渡す
db.Model(&user).Debug().Update("name", "tomoya")

// 3. 構造体インスタンスをModelメソッドに渡し、条件を設定
db.Model(&user).Debug().Where("birthday = ?", birthday).Update("name", "tomoya")

実行されるSQLは以下のようになります。

# 1. 構造体リテラルをModelメソッドに渡し、条件を設定した場合
[1.760ms] [rows:1] UPDATE "users" SET "name"='tomoya',"updated_at"='2025-09-10 23:37:54.584' WHERE id = 1

# 2. 構造体インスタンスをModelメソッドに渡す場合
[1.023ms] [rows:1] UPDATE "users" SET "name"='tomoya',"updated_at"='2025-09-10 23:37:54.586' WHERE "id" = 1

# 3. 構造体インスタンスをModelメソッドに渡し、条件を設定した場合
[1.107ms] [rows:1] UPDATE "users" SET "name"='tomoya',"updated_at"='2025-09-10 23:37:54.587' WHERE birthday = '2025-09-03 00:00:00' AND "id" = 1

UpdateメソッドはWHERE句を使って絞り込みをしないとエラーが発生します。
Global Updatesを防ぐためです。

一方、ジェネリクスAPIを用いた単一カラムの更新は、以下のように実装することができます。

main.go
// 更新に関わる部分のみ抽出
ctx := context.Background()

birthday, _ := time.Parse(time.DateOnly, "2025-09-03")
user := User{ID: 1, Name: "Tomoya", Birthday: birthday}

// 1. 条件を一つ設定
row, err := gorm.G[User](db.Debug()).Where("id = ?", user.ID).Update(ctx, "name", "Tomoya")
if err != nil {
    log.Fatal(err)
}
fmt.Println("rowsAffected:", row) // 出力結果: rowsAffected: 1

// 2. 条件を複数設定
row, err := gorm.G[User](db.Debug()).Where("id = ? AND birthday = ?", user.ID, user.Birthday).Update(ctx, "name", "Tomoya")
if err != nil {
    log.Fatal(err)
}
fmt.Println("rowsAffected:", row) // 出力結果: rowsAffected: 1

実行されるSQLは以下のようになります。

# 1. 条件を一つ設定
[3.699ms] [rows:1] UPDATE "users" SET "name"='Tomoya',"updated_at"='2025-09-10 23:56:27.266' WHERE id = 1

# 2. 条件を複数設定
[1.514ms] [rows:1] UPDATE "users" SET "name"='Tomoya',"updated_at"='2025-09-10 23:56:27.27' WHERE id = 1 AND birthday = '2025-09-03 00:00:00'

ジェネリクスAPIのUpdateメソッドのシグネチャは以下の通りです。

generics.go
func (c chainG[T]) Update(ctx context.Context, name string, value any) (rowsAffected int, err error)

Updateメソッドは、指定したカラムに新しい値を設定し、更新を実行するメソッドです。

  • 引数:
  • ctx: コンテキスト、処理のキャンセルやタイムアウトを制御するために使用
  • name: 更新対象のカラム名を文字列で指定
  • value: 設定する新しい値を任意の型で指定可能

戻り値:

  • rowsAffected: 更新された行数、0の場合条件に合致するレコードが存在しないことを示す
  • err: 更新処理中に発生したエラー、正常に更新できた場合はnil

さて、従来の更新ではSaveメソッドを使うことができました。
このメソッドは、

  • 主キーが存在しない場合はCreateを行う
  • 主キーが存在する場合はUpdateを行う
    というUpsertを行うものでした。

しかし、ジェネリクスAPIではSaveメソッドが廃止となりました。
公式ドキュメントには「曖昧さと並行処理の問題」を防ぐと記載しています。
ですので、ジェネリクスAPIを使って更新する場合はUpdateメソッドを使いましょう。

2. 複数カラムの更新

従来の複数カラムの更新は、以下のように実装することができます。

main.go
// 更新に関わる部分のみ抽出
type User struct {
	ID        uint      `gorm:"primaryKey"`
	Name      string    `gorm:"not null"`
	IsAdmin   bool      `gorm:"not null"`
	Birthday  time.Time `gorm:"type:date"`
	UpdatedAt time.Time `gorm:"type:date"`
}

// 1. 構造体を指定した場合
db.Model(&user).Updates(User{Name: "tomoya", IsAdmin: false})

// 2. mapを指定した場合
db.Model(&user).Updates(map[string]interface{}{"name": "tomoya", "is_admin": false})

実行されるSQLは以下のようになります。

# 1. 構造体を指定した場合
[1.530ms] [rows:1] UPDATE "users" SET "name"='tomoya',"updated_at"='2025-09-11 22:01:20.111' WHERE "id" = 1

# 2. mapを指定した場合
[0.889ms] [rows:1] UPDATE "users" SET "is_admin"=false,"name"='tomoya',"updated_at"='2025-09-11 22:01:20.113' WHERE "id" = 1

Updatesメソッドでは、引数に構造体を指定する方法と、mapを指定する方法があります。
両者の最大の違いは、ゼロ値で更新できるかどうかです。

  • 構造体を指定した場合→ゼロ値で更新できない
  • mapを指定した場合→ゼロ値で更新できる
    ただ、構造体を指定した場合はSelectメソッドを使って更新するカラムを指定することで、ゼロ値でも更新することができます。
    また、Omitメソッドを使うことで、更新しないカラムを指定することも可能です。

一方、ジェネリクスAPIを用いた複数カラムの更新は、以下のように実装することができます。

main.go
// 更新に関わる部分だけ抽出
ctx := context.Background()

row, err := gorm.G[User](db.Debug()).Where("id = ?", user.ID).Updates(ctx, User{Name: "Tomoya", IsAdmin: false})
if err != nil {
    log.Fatal(err)
}
fmt.Println("rowsAffected:", row) // 出力結果: rowsAffected: 1

実行されるSQLは以下のようになります。

[2.336ms] [rows:1] UPDATE "users" SET "name"='Tomoya',"updated_at"='2025-09-11 22:12:00.3' WHERE id = 1

ジェネリクスAPIのUpdatesメソッドのシグネチャは以下の通りです。

generics.go
func (c chainG[T]) Updates(ctx context.Context, t T) (rowsAffected int, err error)

Updatesメソッドは、指定したカラムに新しい値を設定し、更新を実行するメソッドです。

  • 引数:
  • ctx: コンテキスト、処理のキャンセルやタイムアウトを制御するために使用
  • t: 型パラメータで、指定された構造体の値を更新対象とする

戻り値:

  • rowsAffected: 更新された行数、0の場合条件に合致するレコードが存在しないことを示す
  • err: 更新処理中に発生したエラー、正常に更新できた場合はnil
    UpdatesメソッドもUpdateメソッドと同じく、ゼロ値での更新はできません。
    更新する必要があれば、Selectメソッドを使ってカラムを指定しましょう。
    また、Omitメソッドを使うことで、更新しないカラムを指定することも可能です。

また、従来のUpdatesメソッドではmapを使った更新ができましたが、ジェネリクスAPIでは出来ないようです。

まとめ

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

  • 単一カラムの更新:Updateメソッド
    • Saveメソッドは廃止になったので注意
  • 複数カラムの更新:Updatesメソッド
    • ゼロ値での更新ができないので注意
    • 必要に応じてSelectメソッドやOmitメソッドを使う

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

参考

Discussion