はじめに
Datastoreで1エンティティ毎Getするのは非効率で、一括で取得することが推奨されているようです。
Stackdriverトレースでも複数回Getしているものは、一括で処理するようにアドバイスされます。
そこで複数回Getしていたものを、GetMultiを使用して一括処理しようとした際にハマったポイントについてまとめました。
まずコードを書いてみる
下記のように実装しましたが、私が想定したように動いてくれませんでした。
func GetUsers(ctx context.Context, userIds []string) ([]*UserKind, err) {
var keys []*datastore.Key
for _, id := range userIds {
keys = append(keys, datastore.NewKey(ctx, "User", id, 0, nil))
}
users := make([]*UserKind, len(keys))
if err := datastore.GetMulti(ctx, keys, users); err != nil && err != datastore.ErrNoSuchEntity {
return nil, err
} else {
for i := 0; i < len(users); i++ {
users[i].Key = keys[i].StringID()
}
return users, nil
}
}
datastore.ErrNoSuchEntityエラーの判定ができない
if err := datastore.GetMulti(ctx, keys, users); err != nil && err != datastore.ErrNoSuchEntity {
return nil, err
}
Getではエンティティが存在しない場合はdatastore.ErrNoSuchEntity
が返却されるため、それ以外のerrが起きた場合のみerrを返却しようとしました。
しかし、上記のコードではエンティティが存在しない際、errはdatastore.ErrNoSuchEntity
とマッチしませんでした。
メッセージをみるとdatastore: no such entity (and 14 other errors)
となっていました。
datastore.ErrNoSuchEntityのようですが、メッセージ末尾の (and 14 other errors)
がマッチしない原因のようです。
appengine.MultiErrorについて
GetMultiで返却されるerrはappengine.MultiError
で複数のerrを表す型でした。
注意が必要なのはMultiErrorはerrだけではなくnil
が含まれる場合もあることです。
GetMultiでは、単一エンティティを取得するerr := datastore.Get(ctx, key, &entity)
をN回実行されたようにkeyとentityとMultiErrorに含まれるerrは対になるようです。
key1に対するエンティティをeintity1とした場合、以下のイメージでentityのリストとerrのリスト(MultiError)が返却されます。
[key1, key2, key3]
[entity1, nil, entity3]
[nil, err2, nil] <- MultiErrorを展開したイメージ
key2に対するエンティティが存在しない場合はMultiError内の2番目のerrとしてdatastore.ErrNoSuchEntity
が返却されました。エンティティは存在しないのでentityのリストの2番目はnilになります。
逆にkey1での取得が正常にできた場合、entityのリストの1番目はentity1が取得され、MultiError内の1番目はnilになります。
これを踏まえてエラー処理を書くと以下のようになりました。
merr, ok := err.(appengine.MultiError)
if !ok {
//appengine.MultiErrorではない場合はそのエラーを返却
return nil, err
}
for _, e := range merr {
if e == nil {
//entityが存在する
continue
}
if e == datastore.ErrNoSuchEntity {
//entityがないだけなのでスルー
continue
}
//ここまで来ると datastore.ErrNoSuchEntity 以外のエラー
return nil, err
}
また、GetMultiで取得時に指定したKeyに対するusers
が存在しない場合、取得したエンティティのリストにnilが含まれてしまうため、返却用のスライスを別途作成して返却するようにしました。
res := []*UserKind
for i := 0; i < len(users); i++ {
if users[i] != nil {
users[i].Key = keys[i].StringID() // Keyをセット
append(res, users[i])
}
}
return res, nil
## おわりに
そもそもKeyに対するエンティティが存在しないということが前提条件で保証されているのであれば、ここまでの実装は不要ですが、GetMultiは結構ハマりポイントがありましたのでご注意ください。