ちょっとまとめておこうかと。
間違っているところがあったらブログのコメント 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 を返すようにするか、
アプリケーション独自のエラーを定義してあげて、
クライアントコードではそれをハンドリングしてあげる方がいいと思います。