実行時にDynamoDBのテーブル名を変える

##はじめに

このエントリはAWS SDK for Javaが対象です。

大変便利なAWSのDynamoDBですが、テーブル名に関しては以下のような制約があります。

- 1AWSアカウント/1リージョン内には同名のテーブルが持てない
- アクセスユーザーごとに名前空間を変えたりできない

例えばあるシステムがあり、「Hoge」という名前のテーブルにアクセスするプログラムを作っていた場合、同じAWSアカウント/リージョン内に同じシステムを別のユーザーに提供しないといけないようなケースが出てくると、いきなり詰みます。

これを回避するためにはDynamoDBのテーブル名にプレフィックスをつけるなどして名前の衝突を避ける必要がありますが、私のようにそんなことを考えずに作ってしまった場合は、何かしらの方法でテーブル名を差し替える必要が出てきます。

##DynamoDB Mapperの機能
AWS SDK for Javaには、大抵のアクセスに高機能なDynamoDBアクセスを提供するDynamoDB Mapperが提供されています。
このMapperには、テーブル名を差し替える機能が付いています。

Mapperをインスタンス化する際に渡す、もしくはメソッド呼び出し時に渡すDynamoDBMapperConfigクラスに、ObjectTableNameResolver、TableNameResolverそしてTableNameOverrideのプロパティが用意されています。

各クラスにはテーブル名を変換するメソッドが用意されており、DynamoDBMapperConfigにセットしておくと、DynamoDBアクセス時に呼び出され、@DynamoDBTableで定義したテーブル名を変換することができます。

Mapperのインスタンス化時の引数で渡すDynamoDBMapperConfigは、各メソッド呼び出し時に渡すDynamoDBMapperConfigとマージされるため、Mapperのインスタンスを１箇所で行っていれば、比較的容易にシステム全体のテーブル名を変更することができます。

##Mapper以外のクライアント
とはいえDynamoDB Mapperしか使っていないケースは、結構まれではないかと思います。Mapperにはテーブル作成/削除のAPIがないため、別のクラスを使用する必要があります。
Mapper以外には「DynamoDB」と「AmazonDynamoDB」のクライアントクラスがあります。

「DynamoDB」のほうは、DocumentAPIと銘打っており、Tableクラスを取り出して操作するような形態を取っており、Table自体の作成なども含め、Mapperより幅広いAPIを提供しています。

また「AmazonDynamoDB」は一番プリミティブなAPIを提供しています。

##Mapper、DynamoDB、AmazonDynamoDBを混在で使っていた場合
Mapperにはテーブル名変換の機能がありましたが、DynamoDBおよびAmazonDynamoDBにはその機能がありません。
従って、順当にやろうとするとAPI呼び出し前にテーブル名を置換するなど泥臭い作業が必要になります。

テーブル名を変更する前提で実装を進めていればまだしも、後でテーブル名変更作業をする場合、これはかなり辛い作業になります。なんとかMapperのように、クライアントクラスだけで解決する方法が欲しいところです。

##AmazonDynamoDBに手を加える
Mapper、DynamoDB、AmazonDynamoDBの３つのクライアントがありますが、MapperとDynamoDBは最終的にAmazonDynamoDBに処理を移譲しています。このため、AmazonDynamoDBの中でテーブル名変換を実装すれば、３つのクライアント共通でテーブル名を変換することができます。

AmazonDynamoDBには実装クラスとして、AmazonDynamoDBClientとAmazonDynamoDBAsyncClientが提供されています。今回はAmazonDynamoDBClientを元に手を加えます。

AmazonDynamoDBClientを見ると、引数の違う同名のメソッドが並んでいますが、最終的にはXXXRequestを引数にとるメソッド（例えばdeleteItemであれば、DeleteItemRequestが引数のメソッド）が呼び出されるため、この部分をオーバーライドします。


```java
public class TableNameConvertAmazonDynamoDBClient extends AmazonDynamoDBClient {

  private static Log log = LogFactory.getLog(TableNameConvertAmazonDynamoDBClient.class);

  private DynamoDBTableNameConverter converter;

  public TableNameConvertAmazonDynamoDBClient(AWSCredentials credential, DynamoDBTableNameConverter converter) {
    super(credential);
    this.converter = converter;
  }

  public TableNameConvertAmazonDynamoDBClient(AWSCredentialsProvider provider, DynamoDBTableNameConverter converter) {
    super(provider);
    this.converter = converter;
  }

  public TableNameConvertAmazonDynamoDBClient(ClientConfiguration config, DynamoDBTableNameConverter converter) {
    super(config);
    this.converter = converter;
  }

  public TableNameConvertAmazonDynamoDBClient(AWSCredentials credential, ClientConfiguration config,
      DynamoDBTableNameConverter converter) {
    super(credential, config);
    this.converter = converter;
  }

  public TableNameConvertAmazonDynamoDBClient(AWSCredentialsProvider provider, ClientConfiguration config,
      DynamoDBTableNameConverter converter) {
    super(provider, config);
    this.converter = converter;
  }

  public TableNameConvertAmazonDynamoDBClient(AWSCredentialsProvider provider, ClientConfiguration config,
      RequestMetricCollector collector, DynamoDBTableNameConverter converter) {
    super(provider, config, collector);
    this.converter = converter;
  }

  @Override
  public BatchGetItemResult batchGetItem(BatchGetItemRequest arg) {
    final Map<String, KeysAndAttributes> newMap = new HashMap<>();
    final Map<String, String> convertTableName = new HashMap<>();
    arg.getRequestItems().forEach((K, V) -> {
      final String newTableName = converter.convert(K);
      newMap.put(newTableName, V);
      convertTableName.put(newTableName, K);
    });
    arg.setRequestItems(newMap);
    BatchGetItemResult result = super.batchGetItem(arg);
    Map<String, List<Map<String, AttributeValue>>> resultMap = new HashMap<>();
    result.getResponses().forEach((K, V) -> {
      resultMap.put(convertTableName.get(K), V);
    });
    result.setResponses(resultMap);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public BatchWriteItemResult batchWriteItem(BatchWriteItemRequest arg) {
    final Map<String, List<WriteRequest>> newMap = new HashMap<>();
    final Map<String, String> convertTableName = new HashMap<>();
    arg.getRequestItems().forEach((K, V) -> {
      final String newTableName = converter.convert(K);
      newMap.put(newTableName, V);
      convertTableName.put(newTableName, K);
    });
    arg.setRequestItems(newMap);
    BatchWriteItemResult result = super.batchWriteItem(arg);
    revertUnprocessedItems(result, convertTableName);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  protected void revertUnprocessedItems(BatchWriteItemResult result, Map<String, String> convertTableName) {
    Map<String, List<WriteRequest>> unprocessedMap = new HashMap<>();
    result.getUnprocessedItems().forEach((K, V) -> {
      unprocessedMap.put(convertTableName.get(K), V);
    });
    result.setUnprocessedItems(unprocessedMap);
  }

  @Override
  public CreateTableResult createTable(CreateTableRequest arg) {
    final String actualTableName = converter.convert(arg.getTableName());
    log.info("createTable. actual table name=" + actualTableName);
    arg.setTableName(actualTableName);
    CreateTableResult result = super.createTable(arg);
    revertTableDescription(result.getTableDescription());

    return result;
  }

  @Override
  public DeleteItemResult deleteItem(DeleteItemRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    DeleteItemResult result = super.deleteItem(arg);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public DeleteTableResult deleteTable(DeleteTableRequest arg) {
    final String actualTableName = converter.convert(arg.getTableName());
    log.info("deleteTable. actual table name=" + actualTableName);
    arg.setTableName(actualTableName);
    DeleteTableResult result = super.deleteTable(arg);
    revertTableDescription(result.getTableDescription());
    return result;
  }

  @Override
  public DescribeTableResult describeTable(DescribeTableRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    DescribeTableResult result = super.describeTable(arg);
    revertTableDescription(result.getTable());
    return result;
  }

  @Override
  public GetItemResult getItem(GetItemRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    GetItemResult result = super.getItem(arg);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public ListTablesResult listTables(ListTablesRequest arg) {
    ListTablesResult result = super.listTables(arg);
    if (result.getLastEvaluatedTableName() != null) {
      result.setLastEvaluatedTableName(converter.revert(result.getLastEvaluatedTableName()));
    }
    if (result.getTableNames() != null) {
      result.setTableNames(
          result.getTableNames().stream().map(t -> converter.revert(t)).collect(Collectors.toList()));
    }
    return result;
  }

  @Override
  public PutItemResult putItem(PutItemRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    PutItemResult result = super.putItem(arg);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public QueryResult query(QueryRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    QueryResult result = super.query(arg);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public ScanResult scan(ScanRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    ScanResult result = super.scan(arg);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public UpdateItemResult updateItem(UpdateItemRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    UpdateItemResult result = super.updateItem(arg);
    revertConsumedCapacity(result.getConsumedCapacity());
    return result;
  }

  @Override
  public UpdateTableResult updateTable(UpdateTableRequest arg) {
    arg.setTableName(converter.convert(arg.getTableName()));
    UpdateTableResult result = super.updateTable(arg);
    revertTableDescription(result.getTableDescription());
    return result;
  }

  protected void revertTableDescription(TableDescription desc) {
    final String tableName = desc.getTableName();
    if (tableName != null) {
      desc.setTableName(converter.revert(tableName));
    }
    final String tableArn = desc.getTableArn();
    if (tableArn != null) {
      String prefix = tableArn.substring(0, tableArn.lastIndexOf("/") + 1);
      desc.setTableArn(prefix + converter.revert(tableName));
    }
  }

  protected void revertConsumedCapacity(List<ConsumedCapacity> list) {
    if (list != null) {
      list.forEach(this::revertConsumedCapacity);
    }
  }

  protected void revertConsumedCapacity(ConsumedCapacity consumedCapacity) {
    if (consumedCapacity != null && consumedCapacity.getTableName() != null) {
      consumedCapacity.setTableName(converter.revert(consumedCapacity.getTableName()));
    }
  }
}

```

テーブル名が出てくるところを、コンストラクタで受け取るTableNameConveterのインスタンスで変換します。
またDynamoDBから戻って来るデータの中のテーブル名は、逆変換で戻します。

```java
public interface DynamoDBTableNameConverter {
	String convert(String tableName);
	String revert(String convertedTableName);
}
```

上記インターフェースを実装して、プレフィックスをつけたりテーブル名を丸ごと変えたりすることができます。

このAmazonDynamoDBClientをMapperやDynamoDBに渡せば、画一的にテーブル名を変換することができます。

```java
DynamoDBTableNameConverter converter = new DynamoDBTableNameConverter() {
  public String revert(String convertedTableName) {
   return "HOGE-"+convertedTableName;
  }
  public String convert(String tableName) {
    return tableName.substring(5,tableName.length());
  }
};

AmazonDynamoDB dynamoDB = new TableNameConvertAmazonDynamoDBClient(credentials,converter);
DynamoDBMapper mapper = new DynamoDBMapper(dynamoDB);
```

例えばスレッドローカルにプレフィックスを入れれば、ユーザーごとに異なるテーブルを使うこともできますね。