Go
golang
datastore
GoogleCloudPlatform

Cloud DatastoreのGetMultiにはまった話(Go)

はじめに

Datastoreで1エンティティ毎Getするのは非効率で、一括で取得することが推奨されているようです。
Stackdriverトレースでも複数回Getしているものは、一括で処理するようにアドバイスされます。
スクリーンショット 2018-01-18 11.48.47.png

そこで複数回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は結構ハマりポイントがありましたのでご注意ください。