はじめに
Golang + DynamoDB は高速で便利!だが、DynamoDB のアトリビュートと Golang の map の関連が分かりにくかったり、慣れないとハマりどころがけっこうある。
今回は、1MB以上のアイテムリストを取得するクエリを発行する場合のコードを書いてみる。
DynamoDBのテーブル定義
以下のようなテーブルを用意する。
- idをハッシュキーとする
- nameをレンジキーとする
※実際のデータモデルでこんなものは滅多にないと思うが、同じIDに複数の名前が連なっているというモデルを考える。
今回、簡略化するために、同一 ID で異なる 1000Byte の name 1500 レコードある状態で、ID 指定のクエリを発行して、name の一覧を取得するような Golang のソースを書く。
ソースコード
type item struct {
Id string `dynamodbav:"id"`
Name string `dynamodbav:"name"`
}
func main() {
var (
LastEvaluatedKey map[string]*dynamodb.AttributeValue
)
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))
svc := dynamodb.New(sess)
for {
result, err := svc.Query(&dynamodb.QueryInput{
TableName: aws.String("[テーブル]"),
KeyConditionExpression: aws.String("#ID=:id"),
ExpressionAttributeNames: map[string]*string{
"#ID": aws.String("id"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":id": {
S: aws.String("00001"),
},
},
ExclusiveStartKey: LastEvaluatedKey,
})
if err != nil {
log.Println("Got error calling Query:")
log.Println(err.Error())
return
}
log.Println(result)
if len(result.LastEvaluatedKey) == 0 {
break
} else {
LastEvaluatedKey = result.LastEvaluatedKey
}
}
return
}
LastEvaluatedKey
DynamoDB は1回の Query 関数で取得し切れない場合、「ここから続けてね」という情報を QueryOutput 構造体の LastEvaluatedKey に格納して返してくる。
この情報を持ち廻って、Query の ExclusiveStartKey に設定することで、次の Query では続きを取得してくれる。
なお、LastEvaluatedKey が設定される場合は以下のような中身になっている。
LastEvaluatedKey: {
name: {
S: "01022xxxxx(中略)xxxxx"
},
id: {
S: "00001"
}
},
では、このレコードは初回呼び出しと次の呼び出し、どちらに格納されるかというと、初回側に
Items: [
{
id: {
S: "00001"
},
name: {
S: "00001xxxxx(中略)xxxxx"
}
},
(中略)
{
id: {
S: "00001"
},
name: {
S: "01022xxxxx(中略)xxxxx"
}
}
],
といった具合に、Item 配列の最後に同じレコードの情報が格納されていて、2回目呼び出しの Item 配列には上記レコードが含まれていない。要は、重複や漏れを考える必要なく、Item 配列を扱っておけば、取得したアイテムリストはそのまま使えると考えれば良い。
LastEvaluatedKey は初期化した状態で渡せば、クエリのオプションで指定しなかった場合と同等の動作となるため、上記のコードは先に LastEvaluatedKey を宣言しておき、do while 的なループの回し方をしている(Golang には do while がなく for に一本化されているので、上記のような方式となる)。
Lambdaで扱う場合、Golang の var で毎回初期化してくれるかが分からないので、ループに入る前に一度初期化しておくのが安全だろう。
これで、大量データを処理するクエリも問題なく扱えるようになった!