🈳

GORM の Find でレコードがない場合に nil にならなくてハマった

2023/07/04に公開

ベースとなるコード

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 が発生します
https://gorm.io/ja_JP/docs/query.html#単一のオブジェクトを取得する

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
}

FindErrRecordNotFound を返さない点を利用してエラーチェックを省くことが出来たと思いましたが、実行してみたところ返ってきたのは nil ではなく

{
  ID: 0
  Name: ""
}

各型の初期値がセットされた Userstruct でした
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
}
EGSTOCK,Inc.

Discussion