i-icc’s blog

製作物あげたり日記書いたり。

GORM で find する際 where の条件に nil を入れると全取得になってしまう

現象

GORMを使っていて、db.Where("id = ?", nil).Find(&users) みたいなコードを書いたときの話です。

idnil なんだから、当然0件だろうと思っていたら、なぜか全件取得になってしまいレスポンスタイムが遅くなってしまいました。

この挙動の理由を備忘録的に残す記事となります。

環境

gorm: v1.31.0 (本記事で参照するバージョン)

結論

Using Find without a limit for single object db.Find(&user) will query the full table and return only the first object which is non-deterministic and not performant

Query | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

GORMの公式ドキュメントでは、条件を指定しない db.Find(&user) がテーブル全体をスキャンするため非効率であると警告されています。

実は、Where句にnilを渡した場合も、これと全く同じ状況に陥ります。

原因とソースコード解説

じゃあ、なんでnilだと条件が無視されるのか?

GORMのWhereメソッドは、引数にnilが渡された場合、その条件を意図的に無視するように設計されています。

1. 条件引数のスキップ - statement.go

Whereメソッドに渡された条件と引数は、まず内部のBuildConditionメソッドによって解析されます。

該当コード: https://github.com/go-gorm/gorm/blob/master/statement.go#L323

    for idx, arg := range args {
        if arg == nil {
            continue
        }
}

上記のコードにより、Where("id= ?", nil)という呼び出しでは、nilが引数として渡されるため、"id= ?" という条件式全体がWHERE句の構築プロセスから除外されます。

2. WHERE句の非生成 - chainable_api.go

BuildConditionがnil引数をスキップした結果、有効な条件が一つも生成されなかった場合、空の条件スライス ([]clause.Expression) が返されます。

該当コード: https://github.com/go-gorm/gorm/blob/master/chainable_api.go#L207

// [docs]: https://gorm.io/docs/query.html#Conditions
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
    tx = db.getInstance()
    if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
        tx.Statement.AddClause(clause.Where{Exprs: conds})
    }
    return
}

len(conds)が0になるため、ifブロックは実行されず、SQLにWHERE句は追加されません。

あとは公式ドキュメントに記述されている。下記の通りの仕様でデータが取得されるので全取得となるようです。

Using Find without a limit for single object db.Find(&user) will query the full table and return only the first object which is non-deterministic and not performant

対策

原因がわかれば対策は至ってシンプルで、「Whereに渡す前にnilチェックをちゃんとしよう」という話ですね。

var users []User
if id == nil {
   return users // 空の配列を返す
}
db.Where("id = ?", id).Find(&users)
return users

まとめ

GORMのWhereにnilを渡すと全件取得になっちゃう流れは、

  1. 内部的に引数のnilがスルーされる
  2. 有効な条件がなくなったのでWHERE句が作られない
  3. 結果、ただのFindと同じになって全件取得

というわけでした。

Whereに渡す前にnilチェックをちゃんとしましょうね〜(というかこの仕様なんとかならないかな〜)