🕳️

GormのUpdatesメソッドを使う際の落とし穴〜GormとBunの比較も〜

に公開

はじめに

こんにちは!イノベーション開発チームのmiyaken85です!

Goでバックエンド開発を行っている皆さん、データベース操作で予期しない動作に遭遇したことはありませんか?

本記事では、GoのORMライブラリとして広く使われているGORMUpdatesメソッドを使用する際に遭遇する、boolean型のfalse値や数値型の0などのゼロ値が更新されないという落とし穴について解説します。

この問題は、GORMの仕様を理解していないと遭遇しやすく、実際に僕も開発中にハマってしまった経験があります。

本記事を通じて、同じ問題で悩む方の助けになれば幸いです。

本記事で学べること:

  • GORMの基本的な仕組み
  • Updatesメソッドでゼロ値が更新されない理由
  • 具体的な解決方法
  • 代替となるORM(Bun)の紹介

それでは、順を追って見ていきましょう!

GORMとは?

GORMは、Go言語で最も人気のあるORM(Object-Relational Mapping)ライブラリです。

https://gorm.io/

GORMの特徴

  • フル機能のORM: CRUD操作、マイグレーション、リレーションシップなど、必要な機能がすべて揃っています
  • 開発者フレンドリー: 直感的なAPIで、SQLを書かずにデータベース操作が可能
  • 多様なデータベース対応: PostgreSQL、MySQL、SQLite、SQL Serverなどをサポート
  • アソシエーション: Has One、Has Many、Belongs To、Many to Manyなどの関連付けに対応

基本的な使い方

type User struct {
    ID        uint   `gorm:"primaryKey"`
    Name      string
    Email     string
    IsActive  bool
    Age       int
}

// レコードの作成
db.Create(&User{Name: "田中太郎", Email: "tanaka@example.com", IsActive: true, Age: 30})

// レコードの取得
var user User
db.First(&user, 1)

// レコードの更新
db.Model(&user).Update("Name", "佐藤花子")

一見シンプルで使いやすそうですが、実はUpdatesメソッドには注意すべき挙動があります。

false値でUpdatesメソッドを使ったらうまく更新されなかった話

遭遇した問題

ユーザーのアクティブ状態を管理する機能を実装していた時。IsActiveというboolean型のフィールドをtrueからfalseに更新しようとしたところ、データベースが更新されないという問題に直面しました。

type User struct {
    ID        uint   `gorm:"primaryKey"`
    Name      string
    Email     string
    IsActive  bool   // これをfalseに更新したい
    Age       int
}

// ユーザーのアクティブ状態をfalseに更新しようとする
user := User{ID: 1}
db.Model(&user).Updates(User{IsActive: false})

// しかし、データベースは更新されない...!

同様に、数値型のAge0に更新しようとしても更新されませんでした。

// 年齢を0に更新しようとする
db.Model(&user).Updates(User{Age: 0})

// これも更新されない...!

なぜ更新されないのか?

GORM公式ドキュメントのUpdateセクションに、この挙動について明記されています:

NOTE: When updating with struct, GORM will only update non-zero fields. You might want to use map to update attributes or use Select to specify fields to update

どうやら、構造体を使ってUpdatesを呼び出すと、GORMはゼロ値(zero value)のフィールドを無視するという仕様があるようです。

Goにおけるゼロ値は以下の通りです:

  • bool: false
  • int, int64など: 0
  • string: ""
  • pointer: nil
  • time.Time: 0001-01-01 00:00:00 +0000 UTC

これは、意図しない更新を防ぐための設計ですが、明示的にゼロ値に更新したい場合には問題となります

実際のSQLを確認

// 以下のコードは
db.Model(&user).Updates(User{Name: "山田太郎", IsActive: false, Age: 0})

// 以下のようなSQLを生成します
// UPDATE users SET name = '山田太郎' WHERE id = 1
// IsActiveとAgeは更新されない!

この仕様を知らずに使うと、「なぜ更新されないんだ?」と悩むことになります。

僕もこの問題でかなり時間を費やしてしまいました...😭

解決方法

この問題を解決するために、僕は2つの方法を検討しました。最終的には方法1のmap[string]interface{}を使う方法で解決することができました。

方法1: map[string]interface{}を使う (採用した方法)

最も直接的な解決方法は、構造体の代わりにmap[string]interface{}を使うことです。

db.Model(&user).Updates(map[string]interface{}{
    "is_active": false,
    "age": 0,
})

この方法では、GORMはゼロ値も含めてすべての値を更新します。

メリット:

  • シンプルで分かりやすい
  • 確実にゼロ値を更新できる
  • 実装が簡単で即座に問題を解決できる

デメリット:

  • 型安全性が失われる
  • タイプミスに気づきにくい
  • IDEの補完が効かない

方法2: pointer型を使う方法

もう1つの解決方法として、bool型をpointer型に変更するという方法も検討しました。

type User struct {
    ID        uint    `gorm:"primaryKey"`
    Name      string
    Email     string
    IsActive  *bool   // pointer型に変更
    Age       *int    // pointer型に変更
}

// 更新時
falseValue := false
zeroAge := 0
db.Model(&user).Updates(User{
    IsActive: &falseValue,
    Age: &zeroAge,
})

pointer型にすることで、nilがゼロ値となり、false0は明示的な値として扱われます。

メリット:

  • 構造体をそのまま使える
  • NULL値を表現できる
  • Select()を使わなくても更新可能

デメリット:

  • 構造体定義を変更する必要がある
  • pointer型の扱いに注意が必要(nil参照のリスク)
  • 既存のコードへの影響が大きい

なぜ方法1を採用したか

最終的に**方法1のmap[string]interface{}**を採用した理由は以下の通りです:

  1. 即座に問題を解決できる: 構造体定義の変更やコードの大幅な修正が不要
  2. シンプルで汎用的: 最も直接的で理解しやすく、更新したい値のみ上書きして渡すようにしてるため、一般的なREST APIにおけるUpdateメソッドの役割に沿った作りにできている
  3. 既存コードへの影響が少ない: 構造体定義を変更する必要がない
  4. 確実性: ゼロ値を確実に更新できる
// 採用した書き方
db.Model(&user).Updates(map[string]interface{}{
    "is_active": false,
    "age": 0,
})

他にもあり得た方法

実は、上記の2つの方法以外にも解決策が存在します。後から調べてみて分かったのですが、当時は思いつかなかった方法です。参考までに紹介します。

Select()メソッドを使う方法

Select()メソッドを使うと、更新するフィールドを明示的に指定できます。

db.Model(&user).Select("IsActive", "Age").Updates(User{
    IsActive: false,
    Age: 0,
})

特徴:

  • 型安全性が保たれる
  • 構造体を使える
  • 更新するフィールドを明示的にコントロールできる
  • ただし、少しコードが長くなる

Update()メソッドで単一フィールドを更新

単一のフィールドだけを更新する場合は、Update()メソッドが使えます。

db.Model(&user).Update("is_active", false)
db.Model(&user).Update("age", 0)

特徴:

  • 単一フィールドの更新には最もシンプル
  • ゼロ値も問題なく更新できる
  • 複数フィールドを更新する場合は複数回呼び出す必要がある

代替手法: BunというGoのORMの紹介

とはいえ、updateしたい値の中にfalse値があるとかってシチュエーションは、超一般的だと思い、その度に全フィールドをmapしないといけないのってすごく不便じゃないか。

と思ったので、他にいい方法ないかなと技術顧問の方に伺ったところ、Bunという別のGoのORMライブラリも教えてもらったので調べてみました。

Bunは比較的新しいORMですが、興味深い特徴を持っています。

Bunとは?

Bunは、SQL-firstのアプローチを採用したGoのORMライブラリです。「SQLを隠すのではなく、SQLを活用する」という哲学の下に開発されています。

https://bun.uptrace.dev/

Bunの特徴

  1. SQL-firstアプローチ: 生成されるSQLが予測可能
  2. 高パフォーマンス: 生SQLに近い性能
  3. 型安全: コンパイル時の型チェック
  4. 柔軟性: 必要に応じて生SQLも使える
  5. 段階的導入: 既存のコードベースに徐々に統合可能

Bunでの更新処理

Bunでは、ゼロ値の更新問題はどのように扱われるのでしょうか?

type User struct {
    bun.BaseModel `bun:"table:users"`

    ID       int64  `bun:",pk,autoincrement"`
    Name     string
    IsActive bool
    Age      int
}

// Bunでの更新
_, err := db.NewUpdate().
    Model(&user).
    Column("is_active", "age").
    Where("id = ?", 1).
    Exec(ctx)

Bunでは、明示的にColumn()メソッドで更新するカラムを指定するため、ゼロ値の問題に遭遇しにくい設計になっているらしい。

2つのORMの比較はこんな感じかなと思います。

GORMとBunの比較

項目 GORM Bun
哲学 ORMファースト SQL-first
ゼロ値の扱い 構造体使用時は無視される 明示的な指定が必要
学習曲線 比較的緩やか SQLの知識が必要
パフォーマンス 良好 より高速
コミュニティ 大きい 成長中
ドキュメント 充実 充実

Bunを選ぶべきケース

  • SQLをしっかり理解していて、予測可能なクエリが欲しい場合
  • パフォーマンスを最優先したい場合
  • 既存のdatabase/sqlコードと統合したい場合
  • 新規プロジェクトで、モダンなORMを使いたい場合

GORMを選ぶべきケース

  • 豊富なエコシステムと大きなコミュニティが必要な場合
  • 迅速な開発が求められる場合
  • 既存のGORMを使ったコードベースがある場合
  • SQLの知識が少ないチームメンバーがいる場合

公式ドキュメント: Bun Guide

まとめ

本記事では、GORMのUpdatesメソッドを使う際の落とし穴について解説しました。重要なポイントをまとめます:

📌 重要なポイント

  1. GORMの仕様を理解する

    • 構造体を使ったUpdatesメソッドは、ゼロ値(false、0、""など)を無視する
    • これは意図しない更新を防ぐための設計
  2. 主な解決方法

    • map[string]interface{}を使う(採用)
    • pointer型を使う(構造体定義の変更が必要)
    • その他: Select()、Update()メソッドなど
  3. 採用した方法

    // map[string]interface{}を使う方法
    db.Model(&user).Updates(map[string]interface{}{
        "is_active": false,
        "age": 0,
    })
    

    理由: シンプルで即座に問題を解決でき、既存コードへの影響が少ない

  4. 代替案としてのBun

    • SQL-firstアプローチの新しいORM
    • ゼロ値の問題に遭遇しにくい設計
    • パフォーマンスと予測可能性を重視する場合に最適

最後に

今回紹介した問題、false値のupdateって割と一般的なパターンのため、触って遭遇した人も多いかと思いますが、この仕様を知らないと中々びっくり且つ沼にハマることも多いのかなと思います。

この記事が、同じ問題で悩んでいる方の助けになれば幸いです。

最後まで読んでいただき、ありがとうございました!

参考リンク:


GitHubで編集を提案
株式会社イノベーション Tech Blog

Discussion