GORM の Find でレコードがない場合に nil にならなくてハマった
ベースとなるコード
type User struct {
ID int `gorm:"column:id"`
Name string `gorm:"column:name"`
}
func GetByID(userID user.ID) (*User, error) {
var ret User
if err := db.
Where("user_id = ?", userID).
Take(&ret).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &ret, nil
}
users から user_id で検索するだけのシンプルなコードですが、Take/First/Last を使用する際は、レコードが存在しない場合 ErrRecordNotFound が発生します
GORMは、データベースから1つのオブジェクトを取得するためにFirst, Take, Lastメソッドを提供しています。それらのメソッドは、データベースにクエリを実行する際にLIMIT 1の条件を追加し、レコードが見つからなかった場合、ErrRecordNotFoundエラーを返します。
上記ドキュメントには続けて以下のように書いてあります
ErrRecordNotFound エラーを避けたい場合は、db.Limit(1).Find(&user)のように、Find を 使用することができます。Find メソッドは struct と slice のどちらも受け取ることができます。
レコードがない場合は nil を返したいだけなのに毎回 errors.Is(err, gorm.ErrRecordNotFound) を書くのは冗長で嫌だったので、Find を使うようにしたのですが nil を返せずにハマりました
何が起きたか
func GetByID(userID user.ID) (*User, error) {
var ret User
if err := db.
Where("user_id = ?", userID).
Find(&ret).Error; err != nil {
return nil, err
}
return &ret, nil
}
Find が ErrRecordNotFound を返さない点を利用してエラーチェックを省くことが出来たと思いましたが、実行してみたところ返ってきたのは nil ではなく
{
ID: 0
Name: ""
}
各型の初期値がセットされた User の struct でした
GORM はレコードが取得できない場合でもマッピング対象の struct を返そうとするため、初期化された User が返ってしまい期待していた nil を返せませんでした
どうするべきか
基本的には最初の記述通り Take/First/Last + ErrRecordNotFound の組み合わせで良いと思います(冗長に感じますが…)
どうしても Find を利用する必要がある場合は、以下の様に記述することで同等の挙動にできそうです
func GetByID(userID user.ID) (*User, error) {
var ret User
result := db.
Where("user_id = ?", userID).
Find(&ret)
if result.Error != nil {
return nil, err
}
// rows affected でレコードの有無を確かめる
if result.RowsAffected == 0 {
return nil, nil
}
return &ret, nil
}
Discussion
GormというよりGoの話ですね。
ローカルで定義された構造体(実体)変数から&演算子をつかってポインタを返却しているので、中間がどんなコードになっていてもどんなライブラリを使っていてもnilになることはありえないです。