Datastore/Go の Get(), GetMulti() で指定した key が存在しない場合はエラーになる

ちょっとまとめておこうかと。

間違っているところがあったらブログのコメント or twitter で教えてください。
(´・ω・`)

Datastore では Get() に指定した key の entity が存在しない場合、
datastore.ErrNoSuchEntity というエラーが発生する。
*データが存在しないとエラーになるのはちょっとビックリしました。

なので、以下のようなエラーハンドリングは正常に動作しない。
entity が存在しない場合は datastore.ErrNoSuchEntity が発生するので、
最初の if err != nil で引っかかってしまう。
if entity == nil には到達しない。

err := datastore.Get(ctx, key, &entity)

if err != nil {
    //datastore.ErrNoSuchEntityを補足してしまう
    return
}

if entity == nil {
    //entityが存在しない場合
    return
}

//entityが存在する場合
return



以下のようにdatastore.ErrNoSuchEntityを補足してしまう捕捉するのもダメ。
最初の err != nil で datastore.ErrNoSuchEntity を捕捉してしまう。

err := datastore.Get(ctx, key, &entity)

if err != nil {
    //結局ここでdatastore.ErrNoSuchEntityを補足してしまう
    return
}

if err == datastore.ErrNoSuchEntity {
    //entityが存在しない場合
    return
}

//entityが存在する場合
return



以下のように先に datastore.ErrNoSuchEntity をチェックする必要がある。

err := datastore.Get(ctx, key, &entity)

if err == datastore.ErrNoSuchEntity {
    //entityが存在しない場合
    return
}

if err != nil {
    //Datastoreでエラーが発生した場合
    return
}

//entityが存在する場合
return



ちなみに以下のエラーは if の等号比較で判別可能。
https://github.com/golang/appengine/blob/master/datastore/datastore.go#L20-L28

そして、GetMulti() でも同じように datastore.ErrNoSuchEntity は発生する。

ただ、面倒なことに XxxMulti() 系のAPI
戻り値の error が appengine.MultiError という特殊な error になる。
https://cloud.google.com/appengine/docs/standard/go/datastore/reference#hdr-Basic_Operations

GetMulti, PutMulti and DeleteMulti are batch versions of the Get, Put and Delete functions. They take a []*Key instead of a *Key, and may return an appengine.MultiError when encountering partial failure.



appengine.MultiError は複数の error を扱うためのもので、
実体は error の slice になっている。
https://github.com/golang/appengine/blob/master/errors.go#L25


で、appengine.MultiError の error interface が以下。
https://github.com/golang/appengine/blob/master/errors.go#L27-L46


Multi系APIは発生したエラー件数によって Error() で返す文字列が変わる。
https://github.com/golang/appengine/blob/master/errors.go#L37-L45


エラーが1件だけだと、
その1件の Error() をそのまま返すので、
ローカル環境でテキトーにデバッグしてて、
エラーメッセージだけ確認してると
appengine.MultiError が返ってきてるはずなのに
単に datastore.ErrNoSuchEntity が返ってきてると思ってしまうことがあるかもしれない。
まあ、ないと思うけど。


GetMulti() で datastore.ErrNoSuchEntity が発生した場合、
appengine.MultiError は以下のような構造になる。

appengine.MultiError {
    datastore.ErrNoSuchEntity, 
}

1件でも複数件でも実体が []error であることは変わらない。


ということで、
複数指定した key のうち、
いずれかが存在しなくても正常系だとみなす場合は
以下のようにループで error をチェックして、
datastore.ErrNoSuchEntity をエラーと見なさない実装にする必要がある。

if err := datastore.GetMulti(ctx, keys, &entities); err != nil {
    mErr, ok := err.(appengine.MultiError)

    if !ok {
        //appengine.MultiError ではない場合を捕捉
        return err
    }

    for _, e := range mErr {
        if e == nil {
            //entity が存在する
            continue
        }
        
        if e == datastore.ErrNoSuchEntity {
            //entityが存在しないけど正常系とみなすのでスルー
            continue
        }
        
        //ここまで来ると datastore.ErrNoSuchEntity 以外のエラー
    }
}



ここでポイントになるのが最初の if e == nil のチェック。
これはなくてもよさそうだが、
appengine.MultiError の実体である error は key の数だけ要素数が作られるみたいなので、
そうもいかない。


例えば GetMulti() で key = 1,2,3 を指定して、
key = 1 だけ datastore.ErrNoSuchEntity が発生すると、
以下のような
error になる。
エラーじゃない場合は nil で埋まる。

[]error {
    appengine.ErrNoSuchEntity, //entityがない
    nil, //entityある
    nil, //entityある
}



これを for で回すので、if err == nil のチェックをしないといけない。

*if条件の書き方によっては err == nil のチェック不要になりますが、
 nilで埋まる的な説明したかったので・・・。

そして、以下のように appengine.MultiError ではない場合も捕捉しておく。

mErr, ok := err.(appengine.MultiError)

if !ok {
    //appengine.MultiError ではない場合を捕捉
    return err
}

これは type assertion の判定を捕捉しないのが気持ち悪いというのもあるが、
goon を利用していると、
以下のように PutMulti() に限り、datastore.ErrInvalidKey の場合に appengine.MultiError が返らないので、
GetMulti() でも念のため捕捉しておいたほうがいいと思う。
https://github.com/mjibson/goon/blob/a152700c9dfb114918e9a524376c1cc46bc6cfb1/goon.go#L620

*goon.PutMulti() の挙動については職場のエンジニアに教えてもらいました。
 ありがとうございました。


GetMulti() は以下のように Get() と同じようにチェックしてはいけないので、
Multi系API は少し面倒ですね。

if err := datastore.GetMulti(ctx, keys, &entities); err != nil {
    if err == datastore.ErrNoSuchEntity {
        //appengine.MultiError なので、datastore.ErrNoSuchEntity で捕捉できない
    }
}



GetAll() で Query を指定した検索では datastore.ErrNoSuchEntity が発生しません。
ハンドリング不要です。

ちなみに、Datastore のアクセス部分を抽象化する場合、
datastore.ErrNoSuchEntity をそのまま返すと Datastore に依存してしまうので、
nil を返すようにするか、
アプリケーション独自のエラーを定義してあげて、
クライアントコードではそれをハンドリングしてあげる方がいいと思います。