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になることはありえないです。