AWS DynamoDBをAWS SDK for Java から使ってみる(高レベルAPI編)

この記事は2012年に書かれたものです。最新情報はAWS公式ドキュメントを参照してください。

前回の記事ではDynamoDBのサンプルプログラムを例としてAWS SDK for Javaの低レベルAPIからDynamoDBを利用する方法を紹介しました。
今日はもう1つのAPI「高レベルAPI」をDynamoDBのドキュメントに掲載されているサンプルプログラムに沿って使ってみた時のメモを載せます。

高レベルAPIの概要やサンプルプログラムはこちらから。

概要

高レベルAPIではクラスとDynamoDBのテーブルをマッピングすることができます。個別のオブジェクトのインスタンスはテーブルに含まれるアイテムに該当します。

高レベルAPIではCRUD操作、クエリとスキャンの実行をサポートしています。テーブルの追加・削除・変更はできません。これらの操作は低レベルAPIでのみサポートされています。

JavaのオブジェクトとDynamoDBのテーブルのマッピング

マッピングにはアノテーションとDynamoDBMapperクラスがポイントになります。

アノテーション

JavaのオブジェクトとDynamoDBのテーブルをマッピングするにはJavaアノテーションを使います。

  • 必須のアノテーションはDynamoDBTableとDynamoDBHashKeyの2つです。
  • デフォルトでは、クラスのプロパティはDynamoDBのテーブルの同じ名前の属性にマッピングされます。
  • 異なる名前でマッピングしたい時は @DynamoDBAttribute アノテーションを使います。
  • どの属性にもマッピングしたくないプロパティがあれば@DynamoDBIgnore アノテーションを付けます。
  • 整数型や文字列型のような型以外をマッピングする場合は、任意の型をAmazonDynamoDBで利用可能な型に変換するためのConverterを用意する必要があります。
  • DynamoDBでは楽観的ロックのためのversionフィールドをサポートしています。

アノテーションの例はこんな感じ。アノテーションで対応するDynamoDBのテーブル名や属性を指定します。

	
	//テーブル名をマッピング
    @DynamoDBTable(tableName="ProductCatalog")
    public static class CatalogItem {
        private Integer id;
        private String title;
        private String ISBN;
        private Set<String> bookAuthors;

		//ハッシュキーをマッピング
        @DynamoDBHashKey(attributeName="Id")
        public Integer getId() { return id; }
        public void setId(Integer id) { this.id = id; }

		//属性をマッピング
        @DynamoDBAttribute(attributeName="Title")
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }

		//属性をマッピング
        @DynamoDBAttribute(attributeName="ISBN")
        public String getISBN() { return ISBN; }
        public void setISBN(String ISBN) { this.ISBN = ISBN;}

		//属性をマッピング
        @DynamoDBAttribute(attributeName = "Authors")
        public Set<String> getBookAuthors() { return bookAuthors; }
        public void setBookAuthors(Set<String> bookAuthors) { this.bookAuthors = bookAuthors; }

        @Override
        public String toString() {
            return "Book [ISBN=" + ISBN + ", bookAuthors=" + bookAuthors
            + ", id=" + id + ", title=" + title + "]";
        }
    }
 //サンプルプログラムはドキュメントより抜粋
 //http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/JavaSDKHighLevel.html
DynamoDBMapperクラス

DynamoDBMapperクラスはDynamoDBのデータベースへのエントリポイントになるクラスです。このクラスのメソッドを利用してCRUD操作をしたり、クエリやスキャンを実行できます。
たとえば、saveメソッドを使って新しいアイテムをDynamoDBに登録する時はこんな感じ。

        //オブジェクトを作る
        CatalogItem item = new CatalogItem();
        item.setId(601);
        item.setTitle("Book 601");
        item.setISBN("611-1111111111");
        item.setBookAuthors(new HashSet<String>(Arrays.asList("Author1", "Author2")));
        
        // 作ったオブジェクトをDynamoDBのアイテムとして保存
        DynamoDBMapper mapper = new DynamoDBMapper(client);
        mapper.save(item);

 //サンプルプログラムはドキュメントより抜粋
 //http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/JavaCRUDHighLevelExample1.html

事前準備

  • Eclipse, AWS Toolkit for Eclipseを使える状態にしておきます
  • 前回の記事で紹介したサンプルテーブル(ProductCatalog, Forum, Thread, Reply)を作成し、データも投入しておきます。
  • 高レベルAPIではテーブルを作成できないので、予め作っておきます。

高レベルAPIを使ってみる

ドキュメントにあるサンプルプログラムを動かしてみましょう。

CRUD (Create, Read, Update, Delete)操作

CRUD操作のサンプルプログラムはこちら。コピー&ペーストして実行できます。

このサンプルプログラムは、まずCatalogItemテーブルに新しいアイテムを登録し(Create)、そのアイテムを更新(Update)します。続いて更新後のアイテムを読み取り(Read)、削除する(Delete)という流れになっています。

読み込みの時は、 DynamoDBMapperConfigでDynamoDBMapperConfig.ConsistentReads.CONSISTENTを指定するともっとも新しいデータが返ってくることが保証されます(読み取り一貫性)。指定をしない場合、結果整合性が保証されています。

    private static void testCRUDOperations() {

		//アイテムを作る
        CatalogItem item = new CatalogItem();
        item.setId(601);
        item.setTitle("Book 601");
        item.setISBN("611-1111111111");
        item.setBookAuthors(new HashSet<String>(Arrays.asList("Author1", "Author2")));
        
        // DynamoDBMapperを使ってアイテムをDynamoDBに保存
        DynamoDBMapper mapper = new DynamoDBMapper(client);
        mapper.save(item);
        
        // 保存したアイテムをDynamoDBから読み込み
        CatalogItem itemRetrieved = mapper.load(CatalogItem.class, 601);
        System.out.println("Item retrieved:");
        System.out.println(itemRetrieved);

        // 読み込んだアイテムをちょっと更新して再度DynamoDBに保存
        itemRetrieved.setISBN("622-2222222222");
        itemRetrieved.setBookAuthors(new HashSet<String>(Arrays.asList("Author1", "Author3")));
        mapper.save(itemRetrieved);
        System.out.println("Item updated:");
        System.out.println(itemRetrieved);
        
        // 更新したデータをDynamoDBから読み込み
		// DynamoDBMapperConfigで、
        // DynamoDBMapperConfig.ConsistentReads.CONSISTENTを指定しているので
        // もっとも新しいデータが返ってくることが保証されます
        DynamoDBMapperConfig config = new DynamoDBMapperConfig(DynamoDBMapperConfig.ConsistentReads.CONSISTENT);
        CatalogItem updatedItem = mapper.load(CatalogItem.class, 601, config);
        System.out.println("Retrieved the previously updated item:");
        System.out.println(updatedItem);
        
        // 先ほど更新したデータを削除
        mapper.delete(updatedItem);
        
        // 削除済みのデータを読み込もうとしてみます
        CatalogItem deletedItem = mapper.load(CatalogItem.class, updatedItem.getId(), config);

		//nullが返ってくればアイテムは存在しません
        if (deletedItem == null) {
            System.out.println("Done - Sample item is deleted.");
        }
    }
	
	//サンプルプログラムはドキュメントより抜粋
	//http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/JavaCRUDHighLevelExample1.html
QueryとScan

QueryとScanのサンプルプログラムはこちら。こちらもコピー&ペーストして実行できます。サンプルプログラムでは、内部クラスを使って親子関係があるデータを表現しています。

前回のエントリで紹介したように、QueryとScanでは検索スピードが異なります。プライマリキーで検索するQueryの方が高速です。

Queryを使うときの注意事項としては、プライマリキーがハッシュキーのみの場合は DynamoDBMapperクラスのqueryメソッドが使えない、ということがあります。この場合はloadメソッドを使います。プライマリキーがレンジキーの時はqueryメソッドを使います。

queryメソッドもscanメソッドも "lazy-loaded" コレクションを返します。最初に1件だけ検索結果を返し、次の検索結果が必要になった時に読み込みを行います。

レンジキーでqueryする時の例。

    private static void FindRepliesInLast15Days(DynamoDBMapper mapper,
                                                String forumName,
                                                String threadSubject) throws Exception {
        System.out.println("FindRepliesInLast15Days: Replies within last 15 days.");

        String hashKey = forumName + "#" + threadSubject;

        long twoWeeksAgoMilli = (new Date()).getTime() - (15L*24L*60L*60L*1000L);
        Date twoWeeksAgo = new Date();
        twoWeeksAgo.setTime(twoWeeksAgoMilli);
        String twoWeeksAgoStr = dateFormatter.format(twoWeeksAgo);

		//検索条件を作成
        //プライマリキーのレンジ属性を15日前の日付と比較します。
        //比較演算子にはGT : Greater thanを使用
        Condition rangeKeyCondition = new Condition()
                .withComparisonOperator(ComparisonOperator.GT.toString())
                .withAttributeValueList(new AttributeValue().withS(twoWeeksAgoStr.toString()));

		//ハッシュキーを設定
        DynamoDBQueryExpression queryExpression = new DynamoDBQueryExpression(
                new AttributeValue().withS(hashKey));

		//レンジキーの検索条件を設定
        queryExpression.setRangeKeyCondition(rangeKeyCondition);

		//検索結果を取得
		// "lazy-loaded" コレクションで返ってきます。
        List<Reply> latestReplies = mapper.query(Reply.class, queryExpression);

        for (Reply reply : latestReplies) {
            System.out.format("Id=%s, Message=%s, PostedBy=%s %n, ReplyDateTime=%s %n",
                    reply.getId(), reply.getMessage(), reply.getPostedBy(), reply.getReplyDateTime() );
        }
       }

	//サンプルプログラムはドキュメントより抜粋
	//http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/JavaQueryScanORMModelExample.html

Scanの例。高レベルAPIの解説ドキュメントにあるコードスニペットが分かりやすいのでこちらを紹介します。検索条件と、それを適用する属性をMapに保存し、検索を行う例です。

 //回答が付いていないフォーラムスレッドを検索する
 private static void FindUnansweredThread(DynamoDBMapper mapper,
								            String forumName,
								            String threadSubject) throws Exception{

	    DynamoDBScanExpression scanExpression = new DynamoDBScanExpression();

	    Map<String, Condition> scanFilter = new HashMap<String, Condition>();

		//検索条件を作る
	    Condition scanCondition = new Condition()
	        .withComparisonOperator(ComparisonOperator.EQ.toString())
	        .withAttributeValueList(new AttributeValue().withN("0"));

		//検索対象とする属性をKey,検索条件をValueとしてMapに保存
	    scanFilter.put("Answered", scanCondition);
		
		//Mapに保存した検索条件を適用して検索
	    scanExpression.setScanFilter(scanFilter);
	    List<Thread> unansweredThreads = mapper.scan(Thread.class, scanExpression);

	    for (Thread thread : unansweredThreads) {
            System.out.format("Thread=%s, subject=%s,  LastPostedDateTime=%s %n",
                    thread.getForumName(), thread.getSubject(), thread.getLastPostedDateTime() );
        }
    }
	//サンプルプログラムはドキュメントより抜粋
	//http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/JavaSDKHighLevel.html

DynamoDBMapper で指定できるパラメータ

DynamoDBMapperのインスタンスにパラメータを指定すると、オブジェクトをDynamoDBのアイテムにマッピングするときの振る舞いを設定できます。設定にはDynamoDBMapperConfigクラスのインスタンスを利用します。

  • DynamoDBMapperConfig.SaveBehavior
    • DynamoDBMapperConfig.SaveBehavior.UPDATE を指定すると、オブジェクトで変更された属性のみ更新されます。更新していない属性は影響を受けません。デフォルトではこちらが使用されます。
    • DynamoDBMapperConfig.SaveBehavior.CLOBBER を指定すると、オブジェクトで変更された属性以外も影響を受けます。オブジェクトで指定された値ですべて上書きされるので、オブジェクトで設定されていない値は削除されます。ただし、楽観的ロックで使用されるバージョン属性は影響を受けません。
  • DynamoDBMapperConfig.ConsistentReads
    • DynamoDBMapperConfig.ConsistentReads.CONSISTENT を指定すると、もっとも新しいデータが返ってくることが保証されます(読み取り一貫性)。
    • DynamoDBMapperConfig.ConsistentReads.EVENTUAL を指定すると、結果整合性が保証されます。
  • DynamoDBMapperConfig.TableNameOverride
    • DynamoDBTableアノテーションで指定したテーブル名を無視することができます。

まとめ

高レベルAPIを使えばクラスとDynamoDBのテーブルを簡単にマッピングすることができます。マッピングにはアノテーションを使用します。テーブルの作成・変更・削除はできないので注意が必要です。

AWS SDK for Javaの標準機能だけでこれだけいろいろなことができてとても便利です!