こんにちは。松本です。
TECHSCORE Advent Calendar 2014 の 24 日目の投稿です。
前回は eclipse 上で Amazon DynamoDB Local をインストールし、AWS SDK for Java からアイテムの保存を行いました。今回は AWS SDK for Java にフォーカスをあて基本的な API を利用してみます。
環境は引き続き前回セットアップした DynamoDB Local を使います。
高レベル API と低レベル API
PutItemRequest
クラスを使った前回のサンプルコードは「低レベル API」と呼ばれる API で、AWS SDK for Java では DynamoDB を操作するために「高レベル API」を選択することも可能です。
低レベル API
低レベル API は、基本的な DynamoDB オペレーションに緊密に対応しています。低レベル API を使用すると、DynamoDB オペレーションを使用して実行できるオペレーション(テーブルの作成、更新、削除、および項目の作成、読み込み、更新、削除など)と同じものを実行できます。高レベル API
高レベル API では、オブジェクト永続性プログラミング手法を使用して、Java オブジェクトを DynamoDB テーブルと属性にマッピングします。高レベル API を使用してテーブルを作成することはできませんが、テーブル項目の作成、読み込み、更新、および削除は可能です。
普段、リレーショナルデータベースへのアクセスに ORM を使う機会が多い方には高レベル API の方が馴染みやすいと思います。
では前回のサンプルコードを高レベル API で書くとどうなるでしょう。
高レベル API でアイテムを保存する
高レベル API では POJO を DynamoDB のテーブルにマッピングし、DynamoDBMapper
クラスで操作します。
1. マッピング
まずは前回のサンプルで使用した Channels テーブルを Channel
クラスにマッピングします。
com/techscore/dynamodb_local_sample/Channel.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package com.techscore.dynamodb_local_sample; import java.time.OffsetDateTime; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshalling; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; @DynamoDBTable(tableName = "Channels") public class Channel { @DynamoDBHashKey private String channelName; @DynamoDBAttribute private long maxMessageNumber; @DynamoDBAttribute @DynamoDBMarshalling(marshallerClass = OffsetDateTimeMarshaller.class) private OffsetDateTime createdAt; @DynamoDBAttribute @DynamoDBMarshalling(marshallerClass = OffsetDateTimeMarshaller.class) private OffsetDateTime updatedAt; public String getChannelName() { return channelName; } public void setChannelName(String channelName) { this.channelName = channelName; } ..(snip).. } |
実際は各フィールドに対するプロパティも必要ですので適宜、Getter / Setter メソッドを定義して下さい。
マッピング方法は見たまま。@DynamoDBTable
アノテーションでクラスとテーブルのマッピングを行い、@DynamoDBHashKey
アノテーションで HashKey を、@DynamoDBAttribute
アノテーションで属性をマッピングしています。
もし RangeKey をマッピングしたいなら @DynamoDBRangeKey
アノテーションを、マッピングしたくないフィールドやプロパティがあれば、@DynamoDBIgnore
アノテーションを使います。
@DynamoDBMarshalling
アノテーションは高レベル API で属性のデータ型としてサポートされていないクラスを扱うためのアノテーションです。上記例では java.time.OffsetDateTime
を扱うため、独自に作成した OffsetDateTimeMarshaller
クラスを使うことを定義しています。
OffsetDateTimeMarshaller
クラスは DynamoDBMarshaller
インターフェース(javadoc)を実装した次のようなクラス定義となります。
com/techscore/dynamodb_local_sample/OffsetDateTimeMarshaller.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.techscore.dynamodb_local_sample; import java.time.OffsetDateTime; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshaller; public class OffsetDateTimeMarshaller implements DynamoDBMarshaller { @Override public String marshall(OffsetDateTime getterReturnResult) { return getterReturnResult.toString(); } @Override public OffsetDateTime unmarshall(Class clazz, String obj) { return OffsetDateTime.parse(obj); } } |
2. データの操作
高レベル API でのデータ操作は非常にシンプル。アイテムを保存するだけなら DynamoDBMapper
の save()
メソッドにオブジェクトを渡すだけです。
com/techscore/dynamodb_local_sample/ChannelCreationHighLevelAPI.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.techscore.dynamodb_local_sample; import java.time.OffsetDateTime; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBSaveExpression; import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; public class ChannelCreationHighLevelAPI { public static void main(String[] args) { AWSCredentials credentials = new BasicAWSCredentials(("yourAccessKeyId", "yourSecretAccessKey"); AmazonDynamoDBClient client = new AmazonDynamoDBClient(credentials); client.setEndpoint("http://localhost:8000", "", "local"); DynamoDBMapper mapper = new DynamoDBMapper(client); Channel channel = new Channel(); channel.setChannelName("techscore"); channel.setCreatedAt(OffsetDateTime.now()); channel.setUpdatedAt(OffsetDateTime.now()); channel.setMaxMessageNumber(0); mapper.save(channel); } } |
DynamoDBClient
の生成までは低レベル API の時と同じです。
尚、DynamoDBMapper
オブジェクトはスレッドセーフなので、実際のアプリケーションではスレッド間で共有することも可能です。
This class is thread-safe and can be shared between threads. It's also very lightweight, so it doesn't need to be.
Conditional Writes
DynamoDB では Conditional Writes を使うことで、アイテムや属性の更新を条件付きで実行することが可能です。
To help clients coordinate writes to data items, DynamoDB supports conditional writes for PutItem, DeleteItem, and UpdateItem operations. With a conditional write, an operation succeeds only if the item attributes meet one or more expected conditions; otherwise it returns an error.
これはマルチユーザー環境で同じ属性に同時アクセスされた時のデータの整合性を制御するものです。今回は身近なサンプルとして、同じプライマリキーを持つアイテムが存在しない場合のみアイテムを新規保存するコードを考えてみます。
実は低レベル API の PutItemRequest
や高レベル API の save()
メソッドは一致するプライマリキーを持つアイテムが存在しなければアイテムを新規保存し、存在すればアイテムや属性を更新します。これを新規保存のみの操作に限定してみましょう。
1. 低レベル API での例
低レベル API でのサンプルとして前回のサンプルコードを流用します。
アイテム保存時の条件に「テーブル内に新しいアイテムの channelName 属性値と一致するアイテムが無い」という条件を加えるには PutItemRequest
オブジェクトに addExpectedEntry()
を追加します。
1 |
.addExpectedEntry("channelName", new ExpectedAttributeValue().withExists(false)); |
ExpectedAttributeValue
オブジェクトに withExists(false)
とすることで「指定した属性値が一致するアイテムが存在しないこと」という条件を表現しています。
ソースコード全体としてはこうなります。
com/techscore/dynamodb_local_sample/ChannelCreationLowLevelAPI.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.techscore.dynamodb_local_sample; import java.time.OffsetDateTime; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; import com.amazonaws.services.dynamodbv2.model.PutItemRequest; public class ChannelCreationLowLevelAPI { public static void main(String[] args) { AWSCredentials credentials = new BasicAWSCredentials(("yourAccessKeyId", "yourSecretAccessKey"); AmazonDynamoDBClient client = new AmazonDynamoDBClient(credentials); client.setEndpoint("http://localhost:8000", "", "local"); PutItemRequest request = new PutItemRequest() .withTableName("Channels") .addItemEntry("channelName", new AttributeValue("techscore")) .addItemEntry("maxMessageNumber", new AttributeValue().withN("0")) .addItemEntry("createdAt", new AttributeValue(OffsetDateTime.now().toString())) .addItemEntry("updatedAt", new AttributeValue(OffsetDateTime.now().toString())) .addExpectedEntry("channelName", new ExpectedAttributeValue().withExists(false)); client.putItem(request); } } |
このコードを実行すると、Channels テーブル内に channelName 属性値が "techscore"
であるアイテムが存在する場合、ConditionalCheckFailedException
がスローされアイテムの保存に失敗します。
1 2 3 4 5 6 7 8 |
Exception in thread "main" com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException: The conditional request failed (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException; Request ID: 6353db04-93b6-48e7-9df8-0be8dd8516a7) at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:1077) at com.amazonaws.http.AmazonHttpClient.executeOneRequest(AmazonHttpClient.java:725) at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:460) at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:295) at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:3106) at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.putItem(AmazonDynamoDBClient.java:1206) at com.techscore.dynamodb_local_sample.ChannelCreationLowLevelAPI.main(ChannelCreationLowLevelAPI.java:25) |
2. 高レベル API での例
高レベル API では DynamoDBSaveExpression
クラスの withExpectedEntry()
メソッドを使います。
1 2 |
DynamoDBSaveExpression saveExpr = new DynamoDBSaveExpression() .withExpectedEntry("channelName", new ExpectedAttributeValue().withExists(false)); |
低レベル API の時と同様、ExpectedAttributeValue
で withExists(false)
とすることで、指定した属性に該当するアイテムが存在しないことという条件を表現しています。
また、DynamoDBMapper
の save()
メソッドは 2 つ目の引数として DynamoDBSaveExpression
オブジェクトを渡せるものを使います。
1 |
mapper.save(channel, saveExpr); |
ソースコード全体はこうなります。
ChannelCreationHighLevelAPI.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package com.techscore.dynamodb_local_sample; import java.time.OffsetDateTime; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBSaveExpression; import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; public class ChannelCreationHighLevelAPI { public static void main(String[] args) { AWSCredentials credentials = new BasicAWSCredentials("yourAccessKeyId", "yourSecretAccessKey"); AmazonDynamoDBClient client = new AmazonDynamoDBClient(credentials); client.setEndpoint("http://localhost:8000", "", "local"); DynamoDBMapper mapper = new DynamoDBMapper(client); Channel channel = new Channel(); channel.setChannelName("techscore"); channel.setCreatedAt(OffsetDateTime.now()); channel.setUpdatedAt(OffsetDateTime.now()); channel.setMaxMessageNumber(0); DynamoDBSaveExpression saveExpr = new DynamoDBSaveExpression() .withExpectedEntry("channelName", new ExpectedAttributeValue().withExists(false)); mapper.save(channel, saveExpr); } } |
Atomic Counters
Atomic Counters はアイテム更新時に属性値をアトミックにインクリメント/デクリメントする機能です。
DynamoDB supports atomic counters, where you use the UpdateItem operation to increment or decrement the value of an existing attribute without interfering with other write requests.
この機能は現時点で高レベル API には実装されておらず、低レベル API でのみ利用可能となっています。
今回のサンプルでは UpdateItemRequest
を使います。
UpdateItemRequest
は PutItemRequest
と違い、更新時にアイテム全体を置き換えるのではなく、指定した属性のみを更新してくれます(高レベル API でも save()
メソッドに引数として DynamoDBMapperConfig
オブジェクトを渡すことで保存時の挙動を変えることができます)。
まずはソースコード全体から。
com/techscore/dynamodb_local_sample/ChannelModificationLowLevelAPI.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.techscore.dynamodb_local_sample; import java.time.OffsetDateTime; import java.util.Map; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; import com.amazonaws.services.dynamodbv2.model.AttributeAction; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue; import com.amazonaws.services.dynamodbv2.model.ReturnValue; import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; import com.amazonaws.services.dynamodbv2.model.UpdateItemResult; public class ChannelModificationLowLevelAPI { public static void main(String[] args) { AWSCredentials credentials = new BasicAWSCredentials("yourAccessKeyId", "yourSecretAccessKey"); AmazonDynamoDBClient client = new AmazonDynamoDBClient(credentials); client.setEndpoint("http://localhost:8000", "", "local"); UpdateItemRequest request = new UpdateItemRequest() .withTableName("Channels") .addKeyEntry("channelName", new AttributeValue("techscore")) .addAttributeUpdatesEntry("maxMessageNumber", new AttributeValueUpdate() .withAction(AttributeAction.ADD) .withValue(new AttributeValue().withN("1"))) .addAttributeUpdatesEntry("updatedAt", new AttributeValueUpdate() .withValue(new AttributeValue(OffsetDateTime.now().toString()))) .addExpectedEntry("channelName", new ExpectedAttributeValue() .withExists(true) .withValue(new AttributeValue("techscore"))) .withReturnValues(ReturnValue.UPDATED_NEW); UpdateItemResult result = client.updateItem(request); } } |
このコードでは Conditional Writes でプライマリキーが一致するアイテムが存在する場合のみ属性を更新するよう制限し、Atomic Counters で maxMessageNumber 属性値を増分 1 でインクリメントしています。
AttributeValueUpdate
クラスの withAction()
メソッドに AttributeAction.ADD
を渡すことで属性値をインクリメントすることを指示します。その後の withValue()
はインクリメントの増分が 1 であることを指定しています。
PutItemRequest
の時と同様に、addExpectedEntry()
メソッドで Conditional Writes に関する操作を行っています。
withReturnValues()
メソッドは、DynamoDBClient
の updateItem()
メソッドの戻り値に関する設定で、ReturnValue.UPDATED_NEW
を指定することで更新後のアイテムを戻り値にセットするよう指定しています。
次のようにすることで、インクリメントされた maxMessageNumber 属性値を取得できます。
1 2 |
Map<String, AttributeValue> attributes = result.getAttributes(); String messageNumber = attributes.get("maxMessageNumber").getN(); |
AttributeValue
での値操作では全般的に String
しか使えないのが残念なところです・・・。
最後に
今回は DynamoDB Local とは関係のない記事になってしまいました。