概要
最近、DynamoDBを触る機会がけっこう増えてきた中、AttributeにJSONのような構造をもったオブジェクトを入れたい時に、Map型があることを最近知りました。
(今まではString型のAttributeにJSON文字列を入れて、アプリケーション側でマージして更新したりしてました )
ドキュメント見ると、JSONのキーに対して更新操作もできるんですね!
よっしゃ!これは早速使ってみよう!と思い、使ってみたはいいもののドハマりして、
ドキュメントもなかなかに無かったので、記録を残しておこうと思います。
Map型でどんな構造を持つか
今回ちょっとだけ複雑に下記のような構造を持たせます。
{
"t1": ["a1", "b1", "c1"],
"t2": {
"a2": 10,
"b2": 15,
"b3": 20
},
"t3": ["a3", "b3", "c3"]
}
この構造で t1
のキーに対して更新することを考えていきます。
(上記構造を持つ attribute名は test
とします。)
何も考えずに更新
まずは何も考えずに更新してみます。
val spec = new UpdateItemSpec()
.withPrimaryKey("hash_key_attr", "key", "range_key_attr", "range_key")
.withReturnValues(ReturnValue.UPDATED_NEW)
.withUpdateExpression("SET #test.#t1 = list_append(#test.#t1, :testValue)")
.withNameMap(new NameMap().`with`("#test", "test").`with`("#t1", "t1"))
.withValueMap(new ValueMap().withList(":testValue", "d1"))
table.updateItem(spec)
これを実行するとt1
というキーが存在するときは、リストが更新されて["a1", "b1", "c1", "d1"]
になります。
ところが、t1
というキーが無かったり、そもそもattributeすら存在していないときは下記のようなエラーが吐かれます。
[error] AmazonServiceException: : The document path provided in the update expression is invalid for update (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: 95a34d76-e84e-4a18-ac38-4a344c37c94a) (AmazonHttpClient.java:1383)
[error] com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:1383)
[error] com.amazonaws.http.AmazonHttpClient.executeOneRequest(AmazonHttpClient.java:902)
[error] com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:607)
[error] com.amazonaws.http.AmazonHttpClient.doExecute(AmazonHttpClient.java:376)
[error] com.amazonaws.http.AmazonHttpClient.executeWithTimer(AmazonHttpClient.java:338)
[error] com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:287)
[error] com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:1970)
[error] com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.updateItem(AmazonDynamoDBClient.java:1784)
[error] com.amazonaws.services.dynamodbv2.document.internal.UpdateItemImpl.doUpdateItem(UpdateItemImpl.java:102)
[error] com.amazonaws.services.dynamodbv2.document.internal.UpdateItemImpl.updateItem(UpdateItemImpl.java:86)
[error] com.amazonaws.services.dynamodbv2.document.Table.updateItem(Table.java:218)
...snip...
ドキュメントパスが不正やと。。
ドキュメントパスを考慮した実装にする
今回で言うとドキュメントパスが不正な理由が
-
test
というattributeが存在しない -
test.t1
というドキュメントパスが存在しない
ということなので、これらの存在を考慮した実装にしてみます。
val spec1 = new UpdateItemSpec()
.withPrimaryKey("hash_key_attr", "key", "range_key_attr", "range_key")
.withReturnValues(ReturnValue.UPDATED_NEW)
.withUpdateExpression("SET #test.#t1 = list_append(#test.#t1, :testValue)")
.withConditionExpression("attribute_exists(#test.#t1)")
.withNameMap(new NameMap().`with`("#test", "test").`with`("#t1", "t1"))
.withValueMap(new ValueMap().withList(":testValue", "d1"))
Try(table.updateItem(spec1)).recover {
case e: ConditionalCheckFailedException =>
val spec2 = new UpdateItemSpec()
.withPrimaryKey("hash_key_attr", "key", "range_key_attr", "range_key")
.withReturnValues(ReturnValue.UPDATED_NEW)
.withUpdateExpression("SET ...") // ココ忘れちゃった。。
.withConditionExpression("attribute_exists(#test)")
.withNameMap(new NameMap().`with`("#test", "test").`with`("#t1", "t1"))
.withValueMap(new ValueMap().withList(":testValue", "d1"))
Try(table.updateItem(spec2)).recover {
case e: ConditionalCheckFailedException =>
val spec3 = new UpdateItemSpec()
.withPrimaryKey("hash_key_attr", "key", "range_key_attr", "range_key")
.withReturnValues(ReturnValue.UPDATED_NEW)
.withUpdateExpression("SET #test = :map")
.withNameMap(new NameMap().`with`("#test", "test"))
.withValueMap(new ValueMap().withMap(":map", new ValueMap().withList("t1", "d1"))
table.updateItem(spec3)
}
}
こうやれば上手く更新できますが、ちょっとコードが冗長すぎて泣きそうになります。。
あと、最高3回もDynamoDBにアクセスするのがつらい。。
思い切って初期化しちゃう
そもそもattributeもない状態だったら空のMapをはじめに定義ちゃえばいいじゃん!!
import scala.collection.JavaConversions._
// 初期化
val spec1 = new UpdateItemSpec()
.withPrimaryKey("hash_key_attr", "key", "range_key_attr", "range_key")
.withReturnValues(ReturnValue.UPDATED_NEW)
.withUpdateExpression("SET #test = if_not_exists(#test, :map)")
.withNameMap(new NameMap().`with`("#test", "test"))
.withValueMap(new ValueMap()
.withMap(":map", new ValueMap()
.withList("t1", seqAsJavaList(Seq.empty[String]))
.withMap("t2", mapAsJavaMap(Map.empty[String, Int]))
.withList("t3", seqAsJavaList(Seq.empty[String]))))
Try(table.updateItem(spec1)) match {
case Success(e) => Some(e)
case Failure(_) => None
}
// 更新
val spec2 = new UpdateItemSpec()
.withPrimaryKey("hash_key_attr", "key", "range_key_attr", "range_key")
.withReturnValues(ReturnValue.UPDATED_NEW)
.withUpdateExpression("SET #test.#t1 = list_append(#test.#t1, :testValue)")
.withNameMap(new NameMap().`with`("#test", "test").`with`("#t1", "t1"))
.withValueMap(new ValueMap().withList(":testValue", "d1"))
table.updateItem(spec2)
これで、コードも少しだけシンプルになり、DynamoDBのアクセスも減りました。
ただし、上記コードの場合、下記を注意しなくてはいけません。
- attributeはあるけど、キーだけがないケースが存在する場合、このままだと使えない
- List型じゃないと初期化できない
- StringSet型は空を許可しないらしいので、初期化できませんでした。
最後に
DynamoDBのMap型便利な割に、ちょっと更新のときは癖があるなと感じました。
Mapのキーが無い時の式とかAWS側で持ってくれればいいのに。
(AWSフォーラムに同様のことが書かれていましたが、スルーされてました。。)
参考
http://qiita.com/komiya_atsushi/items/9d4fe044e8c40caabf6f (大変参考にさせていただきました )
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.Modifying.html