Amazon Web Services ブログ

Amazon DynamoDB Transactions を使用して複数のアイテムに調整された変更を加える

近年、NoSQL データベースをリレーショナルデータベース管理システム (RDBMS) の制約から解放するソリューションと見なす組織が増えているため、NoSQL データベースの使用が大幅に増加しています。NoSQL データベースの柔軟性、俊敏性、およびパフォーマンスは、移行をトリガーする主な利点ですが、組織の重要な要件の一部によって RDBMS は同じままでした。

RDMBS は、最も広く知られ議論されている機能であるトランザクションサポートのために、重要なデータを操作する場合 NoSQL データベースよりも好まれます。

Amazon DynamoDB Transactions 使用する理由

多くのアプリケーションでは、1 つ以上の項目に対して「アトミック」またはオールオアナッシングのデータベース操作を必要とするビジネスロジックが常に必要です。これにより、間違った操作が 1 つ発生した場合、すべてのデータベース操作をロールバックできます。通常、このニーズに対処しようとすると、接続されたすべての操作を追跡し、操作を元に戻すという点で、アプリケーションの実装が難しくなります。

Transactions は、re:Invent 2018 で DynamoDB の新機能として発表されました。データベース内のトランザクションのネイティブサポートを提供し、単一の AWS アカウントとリージョンにある複数の項目に ACID (原子性、一貫性、分離性、耐久性) を提供します。

従来、DynamoDB は単一の項目に対してのみ、これらのプロパティをサポートしていました。データの可用性と耐久性を確保するために設計されましたからです。Transactions は、複数の項目で 1 つ以上のテーブルに原子性 (オールオアナッシング) と分離性 (Transactions 同士は影響なし) を追加しました。

さらに、Transactions の ClientRequestToken キーを使用すると、API 呼び出しがべき等になる可能性があるため、複数の同一の呼び出しが同じ操作を再生せず、呼び出しが 1 つの場合と同じ効果が得られます。

Transactions を使用すると、データベース内でロールバック操作について心配したり、苦労したりする必要がなくなります。Transactions は、複数の項目とテーブルにわたってアクションを調整することにより、データの整合性を維持するのに役立ちます。

仕組みの説明

DynamoDB Transactions は現在、TransactWriteItems および TransactGetItems と呼ばれる 2 つの主要な API アクションを提供します複数のアクションを組み合わせて、単一のオールオアナッシング操作として送信できます。

たとえば、ホテルの予約管理アプリケーションをリレーショナルデータベースから DynamoDB に移動することにしました。次の図は、ER 構造を表しています。図には、宿泊客、予約、部屋の 3 つのエンティティがあります。宿泊客と予約の関係は多対多で、予約と部屋の関係も多対多になります。

ここでレガシーテーブルのアプローチを適用し、3 つのレガシーテーブルを 1 つの DynamoDB テーブルにマッピングします。サンプルデータを使用した DynamoDB のホテル予約データモデルを次のテーブルに示します。

HotelManagement 記録タイプ ID (PK) 属性
宿泊客 “John” ActiveReservations : { “501” }
OccupiesRooms : { “20014” }
予約 “501” GuestId: “John”
ReservationStatus: “FULFILLED”
FulfilledByRoom: “20014”
部屋 “R20014” RoomStatus: “OCCUPIED”
RentedToReservation : “501”

サンプルコード

宿泊客は、予約の作成チェックインチェックアウトの 3 種類の操作を実行できます。この投稿では、次の図に示すように、3 つの異なる Java メソッドにラップされた 3 つのシナリオを取り上げています。これによって、DynamoDB のトランザクション機能を示します。 ユーザー名の図では、3 つの操作を順番に実行できます。1 つ目は予約の作成、2 つ目はチェックイン、3 つ目はチェックアウトです。

まず、作業するテーブルを作成する必要があります。

private static void createTable(String tableName) {
        try {
        	// 文字列を保持する 'Id' という名前のプライマリハッシュキーを持つテーブルを作成します
            CreateTableRequest createTableRequest = new CreateTableRequest().withTableName(tableName)
                .withKeySchema(new KeySchemaElement().withAttributeName("Id").withKeyType(KeyType.HASH))
                .withAttributeDefinitions(new AttributeDefinition().withAttributeName("Id").withAttributeType(ScalarAttributeType.S))
                .withProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(1L).withWriteCapacityUnits(1L));

            // テーブルがまだ存在しない場合は作成します
            TableUtils.createTableIfNotExists(dynamoDB, createTableRequest);

            // テーブルが ACTIVE 状態になるのを待ちます
        	TableUtils.waitUntilActive(dynamoDB, tableName);
        	
            // 新しいテーブルについて記述します
            DescribeTableRequest describeTableRequest = new DescribeTableRequest().withTableName(tableName);
            TableDescription tableDescription = dynamoDB.describeTable(describeTableRequest).getTable();
            System.out.println("Table Created Successfully.Table Description: " + tableDescription);
        	
		} catch (InterruptedException e) {
        	System.out.println("Occupied thread is interrupted");
		}
    }

予約を作成する

次に、システム内のサンプル予約を表すレコードのセットを作成します。

    private static void createReservation() {
    	
        // 宿泊客の項目を作成します
    	HashMap<String, AttributeValue> guestItem = new HashMap<String, AttributeValue>();
    	guestItem.put("Id", new AttributeValue("John"));
    	guestItem.put("ActiveReservations", new AttributeValue("500"));
    	
        // 部屋の項目を作成します
    	HashMap<String, AttributeValue> roomItem = new HashMap<String, AttributeValue>();
    	roomItem.put("Id", new AttributeValue("R20014"));
    	roomItem.put("RoomStatus", new AttributeValue("FREE"));
        
        // 予約の項目を作成します
    	HashMap<String, AttributeValue> reservationItem = new HashMap<String, AttributeValue>();
    	reservationItem.put("Id", new AttributeValue("500"));
    	reservationItem.put("GuestId", new AttributeValue("John"));
    	reservationItem.put("ReservationStatus", new AttributeValue("PENDING"));

        Put createGuest = new Put().withTableName(TABLE_NAME).withItem(guestItem);
        Put createRoom = new Put().withTableName(TABLE_NAME).withItem(roomItem);
        Put createReservation = new Put().withTableName(TABLE_NAME).withItem(reservationItem);

        Collection<TransactWriteItem> actions = Arrays.asList(
        		new TransactWriteItem().withPut(createGuest),
                new TransactWriteItem().withPut(createRoom),
                new TransactWriteItem().withPut(createReservation));
        
        TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
                .withTransactItems(actions)
                .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

	    // トランザクションを実行し、結果を処理します。
		// 次のコードスニペットは、前に定義したアクションを単一のオールオアナッシング操作として実行する方法を示しています
        dynamoDB.transactWriteItems(createReservationTransaction);
        System.out.println("Create Reservation transaction is successful");
    }

予約を作成操作後のテーブルを以下に示します。

HotelManagement 記録タイプ ID (PK) 属性
宿泊客 “John” ActiveReservations : { “500”}
OccupiesRooms : null
予約 “500” GuestId: “John”
ReservationStatus: “PENDING”
FulfilledByRoom: null
部屋 “R20014” RoomStatus: “FREE”
RentedToReservation : null

チェックイン

次の操作はチェックインです。このトランザクション中に、テーブル内の 3 つの項目 (宿泊客、予約、部屋) がすべて更新されます。

    private static void checkIn() {
    	
    	//updateGuest オブジェクトを作成します
    	HashMap<String, AttributeValue> guestItemKey = new HashMap<String, AttributeValue>();
    	guestItemKey.put("Id", new AttributeValue("John"));
    	
    	HashMap<String, AttributeValue> guestExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	guestExpressionAttributeValues.put(":occupies_rooms", new AttributeValue("R20014"));
    	
    	Update updateGuest = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(guestItemKey)
    			.withUpdateExpression("SET OccupiesRooms = :occupies_rooms")
    			.withExpressionAttributeValues(guestExpressionAttributeValues);

    	
    	//updateRoom オブジェクトを作成します
    	HashMap<String, AttributeValue> roomItemKey = new HashMap<String, AttributeValue>();
    	roomItemKey.put("Id", new AttributeValue("R20014"));
    	
    	HashMap<String, AttributeValue> roomExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	roomExpressionAttributeValues.put(":room_status", new AttributeValue("OCCUPIED"));
    	roomExpressionAttributeValues.put(":rented_to_reservation", new AttributeValue("500"));
    	
    	Update updateRoom = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(roomItemKey)
    			.withUpdateExpression("SET RoomStatus = :room_status, RentedToReservation = :rented_to_reservation")
    			.withExpressionAttributeValues(roomExpressionAttributeValues);

    	//updateReservation オブジェクトを作成します
    	HashMap<String, AttributeValue> reservationItemKey = new HashMap<String, AttributeValue>();
    	reservationItemKey.put("Id", new AttributeValue("500"));
    	
    	HashMap<String, AttributeValue> reservationExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	reservationExpressionAttributeValues.put(":reservation_status", new AttributeValue("FULLFILLED"));
    	reservationExpressionAttributeValues.put(":fullfilled_by_room", new AttributeValue("R20014"));
    	
    	Update updateReservation = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(reservationItemKey)
    			.withUpdateExpression("SET ReservationStatus = :reservation_status, FullfilledByRoom = :fullfilled_by_room")
    			.withExpressionAttributeValues(reservationExpressionAttributeValues);
    	
    	//Transactions の実行
    	Collection<TransactWriteItem> actions = Arrays.asList(
        		new TransactWriteItem().withUpdate(updateGuest),
                new TransactWriteItem().withUpdate(updateRoom),
                new TransactWriteItem().withUpdate(updateReservation));
        
        TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
                .withTransactItems(actions)
                .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

        dynamoDB.transactWriteItems(createReservationTransaction);
        System.out.println("Check-in transaction is successful");
    	
    }

チェックイン操作後のテーブルを以下に示します。

HotelManagement 記録タイプ ID (PK) 属性
宿泊客 “John” ActiveReservations : { “500”}
OccupiesRooms : { “R20014” }
予約 “500” GuestId: “John”
ReservationStatus: “FULFILLED”
FulfilledByRoom: “20014”
部屋 “R20014” RoomStatus: “OCCUPIED”
RentedToReservation : “500”

チェックアウト

最後の操作はチェックアウトです。null または空の属性は DynamoDB では許可されないため、一部の属性は削除されます。

    private static void checkOut() {
    	//updateGuest オブジェクトを作成します
    	HashMap<String, AttributeValue> guestItemKey = new HashMap<String, AttributeValue>();
    	guestItemKey.put("Id", new AttributeValue("John"));
    	
    	Update updateGuest = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(guestItemKey)
    			.withUpdateExpression("REMOVE OccupiesRooms, ActiveReservations");
    	//属性に値がないため、削除されます
    	
    	//updateRoom オブジェクトを作成します
    	HashMap<String, AttributeValue> roomItemKey = new HashMap<String, AttributeValue>();
    	roomItemKey.put("Id", new AttributeValue("R20014"));
    	
    	HashMap<String, AttributeValue> roomExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	roomExpressionAttributeValues.put(":room_status", new AttributeValue("FREE"));
    	
    	Update updateRoom = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(roomItemKey)
    			.withUpdateExpression("SET RoomStatus = :room_status REMOVE RentedToReservation") //Since there is no value in the attribute, it is removed
    			.withExpressionAttributeValues(roomExpressionAttributeValues);
    	
    	//updateReservation オブジェクトを作成します
    	HashMap<String, AttributeValue> reservationItemKey = new HashMap<String, AttributeValue>();
    	reservationItemKey.put("Id", new AttributeValue("500"));
    	
    	HashMap<String, AttributeValue> reservationExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	reservationExpressionAttributeValues.put(":reservation_status", new AttributeValue("CLOSED"));
    	
    	Update updateReservation = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(reservationItemKey)
    			.withUpdateExpression("SET ReservationStatus = :reservation_status")
    			.withExpressionAttributeValues(reservationExpressionAttributeValues);
    	
    	//Transactions の実行
    	Collection<TransactWriteItem> actions = Arrays.asList(
        		new TransactWriteItem().withUpdate(updateGuest),
                new TransactWriteItem().withUpdate(updateRoom),
                new TransactWriteItem().withUpdate(updateReservation));
        
        TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
                .withTransactItems(actions)
                .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

        dynamoDB.transactWriteItems(createReservationTransaction);
        System.out.println("Check-out transaction is successful");
    }

チェックアウト操作後のテーブルを以下に示します。

HotelManagement 記録タイプ ID (PK) 属性
宿泊客 “John” ActiveReservations : {}
予約 “500” GuestId: “John”
ReservationStatus: “CLOSED”
FulfilledByRoom: “20014”
部屋 “R20014” RoomStatus: “FREE”

まとめ

DynamoDB トランザクション API は、追加費用なしで DynamoDB テーブルの項目に ACID を提供することにより、開発者のエクスペリエンスを簡素化します。Transactions は単一リージョン DynamoDB テーブルではデフォルトですべて有効に、グローバルテーブルではオプションで無効に設定されています。接続されたデータベース操作に対する長年のニーズに対応し、DynamoDB のスケール、パフォーマンス、およびエンタープライズに対応する利点をより広範囲のワークロードセットに拡張します。

Transactions の詳細については、Amazon DynamoDB 開発者ガイドをご覧ください。

 


著者について

 

Baris Yasin は、アマゾン ウェブ サービスのシニアソリューションアーキテクトです。

 

 

 

 

Serdar Nevruzoglu はアマゾン ウェブ サービスのソリューションアーキテクトです。