7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DynamoDBのMap型のattributeを更新する時に潜む罠

Posted at

概要

最近、DynamoDBを触る機会がけっこう増えてきた中、AttributeにJSONのような構造をもったオブジェクトを入れたい時に、Map型があることを最近知りました。
(今まではString型のAttributeにJSON文字列を入れて、アプリケーション側でマージして更新したりしてました :sweat:

ドキュメント見ると、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 (大変参考にさせていただきました :bow: )
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.Modifying.html

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?