TableStoreのデータ出力ツール紹介とAPI利用時の注意点に関して

はじめに

こんにちは!エンジニアの倉田です。

今回は前回に引き続きTableStoreに関するブログとなります。
テーブル情報等は前回のものを使用しますので、先に前回のブログを見ていただけると幸いです。

www.sbcloud.co.jp

今回の内容は、私の作成したTableStoreのデータエクスポートツールの紹介と、
APIを使用する際のポイントについてまとめていきます。

TableStoreのデータエクスポートツールの紹介

TableStoreには、2020年7月現在コンソール上からテーブルのデータをファイルにエクスポートするような機能は備わっておりません。
また、10件ずつしか表示されないためどんなデータが入っているのか確認したいような場合に少し手間がかかってしまいます。
そこで、今回私はTableStoreからデータを取得しExcelにエクスポートをするツールを作成しましたので、ツールの共有と使い方を紹介していきます。

諸注意

  • ・ 単体テスト等は実施しておりません
  • ・ 本ツールの使用に対するいかなる損害に対しても一切の責任を負いません
  • ・ 本ツールは予告なく仕様変更、または公開を終了する可能性があります

ファイル

以下、ダウンロードリンクとなります。

「ots-export_<バージョン>.zip」を取得・解凍しご利用ください。

https://github.com/sbc-kurata/sbc-kurata.ots-export/releases/tag/v1.0.1

前提条件

  • ・ ツールを実行する端末にJavaのバージョン8以上がインストールされていること
  • ・ ツールを実行する端末にMicrosoft Excelがインストールされていること
  • ・ Table Storeの操作権限(AliyunOTSReadOnlyAccess等)を持つRAMユーザのAccessKeyが生成されていること

テンプレートファイルの編集

はじめに、出力するExcelのテンプレートファイルの編集をします。

「TableStoreDataTemplate.xlsx」を任意の場所にコピーして開きます。

編集可能な領域は、緑背景色の部分のみでTableのPKと出力したい属性名を列挙していきます。
(取得自体は全列取得しますので読み取りコストにはご注意ください)

f:id:sbc_kuratakz:20200702112121p:plain

列が足りない場合は、挿入をし追加をしてください。
列のあまりがある場合は削除していただいても残していても問題ありません。
以下、編集後の例です。

f:id:sbc_kuratakz:20200702112148p:plain

実行方法

「TableStore-ExportData.jar」を以下のように実行します。

java -jar TableStore-ExportData.jar -r <リージョンID> -i <インスタンス名> -t <テーブル名> -p <テンプレートパス> -ak <アクセスキー> -sk <アクセスキーシークレット>

 

上記は必須パラメータのみの実行例です。コマンドラインオプションの詳細は以下の表を参考にしてください。

必須  オプション名  引数  備考  例 
-r リージョンID -r cn-shanghai
-i インスタンス名 -i test_instance
-t テーブル名 or グローバルセカンダリインデックス名 -t test_table
-p 編集したテンプレートのパス 相対パス・フルパスともに可 -p "D:\work\template\Sample.xlsx"
-ak アクセスキー -ak xxxxxxxx
-sk アクセスキーシークレット -sk yyyyyyyyyy
-o 出力ディレクトリ デフォルトは "./" -o "D:\work\output"
-d なし 取得順を降順とする -d
-l 取得上限数 -l 50
-sc 範囲選択(最小行) INF_MINのPK指定は不要 -sc "{\"Primary1\":\"AAA\",\"Primary3\":111}"
-ec 範囲選択(最大行) INF_MAXのPK指定は不要 -ec "{\"Primary1\":\"CCC\",\"Primary3\":333}"

※ Binary型のPKを範囲指定する場合は、Base64エンコードされた文字列を入力してください。

実行後、「<テーブル名>.xlsx」というファイルが生成されます。
以下は、実行後に作成されたファイルの例です。

f:id:sbc_kuratakz:20200702112404p:plain

フローチャート

今回のツールの簡易フローチャートを掲載します。
自作での取得ツールを作成する際の参考になればと思います。

f:id:sbc_kuratakz:20200702174620p:plain

データ取得ツールに関する説明は以上となります。

API使用時の注意点

アプリケーションでのデータ取得や、自作での取得ツールを作成する際はAPI経由での取得となります。
これ以降は、JavaSDKを用いてAPIを実行するときのポイント等を紹介していきます。
下記リンクはSDKのインストール手順です。

https://www.alibabacloud.com/help/doc-detail/43007.htm

今回紹介するものはTableStoreのAPIでアプリケーションで使用する際によく使われるものとします。

  • ・ テーブル情報を取得する「DescribeTable
  • ・ データを挿入する「PutRow
  • ・ データを更新する「UpdateRow
  • ・ データを削除する「DeleteRow
  • ・ データを取得する「GetRow
  • ・ データを範囲で取得する「GetRange

今回のサンプルコードで使用しているテーブルの定義情報は以下の通りです。

f:id:sbc_kuratakz:20200702174835p:plain

APIクライアント生成

クライアントのクラスは「com.alicloud.openservices.tablestore.SyncClient」であり、TableStore固有のクライアントとなっています。

クライアント生成時に必要なパラメータは、「エンドポイント」「アクセスキー」「シークレットキー」「インスタンス名」の4つです。

TableStoreのパブリックエンドポイントは、以下の通りです。

https://<インスタンス名>.<リージョンID>.ots.aliyuncs.com

 
クライアント初期化のサンプルコードは以下となります。

        String accessKey = "xxxxxxxx";
        String secretKey = "yyyyyyyyy";
        String instanceName = "test_instance";
        String regionId = "cn-shanghai";
        String endPoint = "https://" + instanceName + "." + regionId + ".ots.aliyuncs.com"
        SyncClient client = new SyncClient(endPoint, accessKey, secretKey, instanceName);

DescribeTable

TableStoreのデータ操作をするにあたって重要な処理が「DescribeTable」APIでのテーブル情報の取得になります。
このAPIでは、テーブルの設定情報や作成したインデックス情報等の情報取得することができます。

その中でも重要な項目が、「PK情報」と「事前定義属性情報」の2つです。

これには列名と型名の情報があるため、データの挿入・更新・取得の処理での使用や、アプリケーション側でのチェック処理に使用することが可能となります。

サンプルコードは以下になります

    /**
     * Table情報の取得.
     *
     * @param client TableStoreのApiClient
     * @param tableName テーブル名
     */
    public void sampleDescribeTable(SyncClient client, String tableName) {
        DescribeTableRequest request = new DescribeTableRequest();
        request.setTableName(tableName);

        DescribeTableResponse response = null;

        try {
            response = client.describeTable(request);
        } catch (TableStoreException e) {
            // ハンドリング例 (非検査例外のため以降のサンプルではtry-catch文は省略します)  
            // 404の場合
            if (e.getHttpStatus() == HttpStatus.SC_NOT_FOUND) {
                System.err.println("テーブルが見つかりませんでした。");
            }
            throw e;
        }

        TableMeta meta = response.getTableMeta();

        // プライマリキー情報
        Map<String, PrimaryKeyType> primaryKeyInfo =  meta.getPrimaryKeyMap();

        // 事前定義属性情報
        Map<String, DefinedColumnType> definedColumnInfo = meta.getDefinedColumnMap(); 
        
        // 省略
    }

また、テーブル名の引数をグローバルセカンダリインデックス名にすることで、グローバルセカンダリインデックスの情報を得ることが可能です。

PutRow

PutRowは、データの挿入をするAPIです。

サンプルコードは以下になります。

    /**
     * データの挿入.
     *
     * @param client TableStoreのApiClient
     * @param tableName テーブル名
     */
    public void samplePutRow(SyncClient client, String tableName) {
        // PK生成
        PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        primaryKeyBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.fromString("2020-06-30 14:00:00"));
        primaryKeyBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.fromString("東京"));
        PrimaryKey primaryKey = primaryKeyBuilder.build();

        RowPutChange rowPutChange = new RowPutChange(tableName, primaryKey);

        // 属性列
        rowPutChange.addColumn("Precipitation", ValueUtil.toColumnValue(1));
        rowPutChange.addColumn("WindDirection", ValueUtil.toColumnValue("南"));
        rowPutChange.addColumn("Temperature", ValueUtil.toColumnValue(28));

        // API実行
        client.putRow(new PutRowRequest(rowPutChange));
    }

 
注意点としては、PK生成の部分での定義順番をテーブル定義と合わせる必要があります。
この注意点は、これ以降に紹介するAPIすべてに適用されます。

今回のテーブルの場合に、以下のようにしてPK生成してしまうと実行時にエラーとなってしまいます。

        // PK生成
        PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        primaryKeyBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.fromString("東京"));
        primaryKeyBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.fromString("2020-06-30 14:00:00"));
        PrimaryKey primaryKey = primaryKeyBuilder.build();

 
また、「PrimaryKeyBuilder#addPrimaryKeyColumn」の第二引数ですが、テーブル定義の型によって合わせる必要があります。

        primaryKeyBuilder.addPrimaryKeyColumn("StringPK", PrimaryKeyValue.fromString("あああ"));
        primaryKeyBuilder.addPrimaryKeyColumn("IntegerPK", PrimaryKeyValue.fromLong(111));
        primaryKeyBuilder.addPrimaryKeyColumn("BinaryPK", PrimaryKeyValue.fromBinary("aaa".getBytes()));

 
ちなみに、既にデータが存在している場合の挙動はデフォルトでは上書きとなっています。
既にデータがある場合にエラーとしたい場合は、リクエスト前に以下のコードを追記することで可能です。

        Condition condition = new Condition();
        condition.setRowExistenceExpectation(RowExistenceExpectation.EXPECT_NOT_EXIST);

        rowPutChange.setCondition(condition);

 
この場合、PK存在確認における読み取りコストが発生する点に注意してください。

UpdateRow

UpdateRowは、データの更新をするAPIです。

サンプルコードは以下になります。

    /**
     * データの更新.
     *
     * @param client TableStoreのApiClient
     * @param tableName テーブル名
     */
    public void sampleUpdateRow(SyncClient client, String tableName) {
        // PK生成
        PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        primaryKeyBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.fromString("2020-06-30 14:00:00"));
        primaryKeyBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.fromString("東京"));
        PrimaryKey primaryKey = primaryKeyBuilder.build();

        RowUpdateChange rowUpdateChange = new RowUpdateChange(tableName, primaryKey);

        // 属性の値変更
        rowUpdateChange.put("Precipitation", ValueUtil.toColumnValue(2));
        // 属性の削除
        rowUpdateChange.deleteColumns("Temperature");

        client.updateRow(new UpdateRowRequest(rowUpdateChange));
    }

 
指定したPKがテーブルに存在しない場合はデフォルトではエラーにはならず、設定した値でデータの挿入が行われます。
エラーにしたい場合は、以下のコードを追記します。
追記した場合、PutRow同様にPK存在確認における読み取りコストが発生する点に注意してください。

        Condition condition = new Condition();
        condition.setRowExistenceExpectation(RowExistenceExpectation.EXPECT_EXIST);

        rowUpdateChange .setCondition(condition);

        client.putRow(new PutRowRequest(rowPutChange));

DeleteRow

DeleteRowは、データの削除をするAPIです。

サンプルコードは以下になります。

    /**
     * データの削除.
     *
     * @param client TableStoreのApiClient
     * @param tableName テーブル名
     */
    public  void sampleDeleteRow(SyncClient client, String tableName) {
        PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        primaryKeyBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.fromString("2020-06-30 14:00:00"));
        primaryKeyBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.fromString("東京"));
        PrimaryKey primaryKey = primaryKeyBuilder.build();

        RowDeleteChange rowDeleteChange = new RowDeleteChange(tableName, primaryKey);

        DeleteRowResponse response = client.deleteRow(new DeleteRowRequest(rowDeleteChange));
    }

 
指定したPKがテーブルに存在しない場合でもデフォルトではエラーにはなりません。
UpdateRowで紹介したコードでエラーとすることが可能です。

GetRow

GetRowは、1行のデータを取得するAPIです。

サンプルコードは以下になります。

    /**
     * データの1行取得.
     *
     * @param client TableStoreのApiClient
     * @param tableName テーブル名
     */
    public void sampleGetRow(SyncClient client, String tableName) {
        // PK情報生成
        PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        primaryKeyBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.fromString("2020-06-23 13:00:00"));
        primaryKeyBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.fromString("東京"));
        PrimaryKey primaryKey = primaryKeyBuilder.build();

        // リクエスト生成&実行
        SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(tableName, primaryKey);
        criteria.setMaxVersions(1);
        GetRowResponse getRowResponse = client.getRow(new GetRowRequest(criteria));
        Row row = getRowResponse.getRow();

        // 指定したPKのデータがない場合はRowがnullで返却される
        if (row == null) {
            System.out.println("データは存在しません");
            return;
        }

        // データのパース処理
        Map<String, String> dataMap = new LinkedHashMap<>();
        // PK情報抽出
        for (Map.Entry<String, PrimaryKeyColumn> pk : row.getPrimaryKey().getPrimaryKeyColumnsMap().entrySet()) {
            dataMap.put(pk.getKey(), pk.getValue().getValue().toString());
        }

        // 属性情報抽出
        for (Column column : row.getColumns()) {
            dataMap.put(column.getName(), column.getValue().toString());
        }

        System.out.println(dataMap);
    }

 
注意点はデータのパース部分です。
取得したRowはJavaでは以下のような少し複雑なオブジェクト構造となっています。
そのため、サンプルコードにもありますがPKの値を抽出する場合と、属性の値を抽出する場合で少し書き方が異なります。

{
    "PrimaryKey": [
        {
            "String": "Time",
            "PrimaryKeyColumn": {
                "name": "Time",
                "PrimaryKeyValue": {
                    "Object": "2020-06-23 13:00:00",
                    "PrimaryKeyType": STRING
                }
            }
        },
        {
            "String": "Prefecture",
            "PrimaryKeyColumn": {
                "name": "Prefecture",
                "PrimaryKeyValue": {
                    "Object": "東京",
                    "PrimaryKeyType": STRING
                }
            }
        }
    ],
    "Column": [
        {
            "String(name)": "Precipitation",
            "ColumnValue": {
                "Object": "1",
                "ColumnType": "INTEGER"
            },
            "Long(timestamp)": 1592899020838
        },
        {
            "String": "Temperature",
            "ColumnValue": {
                "Object": "24",
                "ColumnType": "INTEGER"
            },
            "Long": 1592899020838
        },
        {
            "String": "WindDirection",
            "ColumnValue": {
                "Object": "北東",
                "ColumnType": "STRING"
            },
            "Long": 1592899020838
        }
    ]
}

 
また、今回はすべて文字列として抽出するために「ColumnValue#toString」を使用していますが、
Javaの各オブジェクトにパースする場合は別のメソッドを使用することとなります。
最も注意すべきはテーブル定義がINTEGER型の値をパースする場合です。以下にサンプルを示していますが、Long型にパースされます。

            // STRINGの場合 (StringAttributeNameは属性名の例)  
            String stringValue = row.getLatestColumn("StringAttributeName").getValue().asString();
            // INTEGERの場合  
            Long intagerValue = row.getLatestColumn("IntegerAttributeName").getValue().asLong();
            // DOUBLEの場合  
            Double doubleValue = row.getLatestColumn("DoubleAttributeName").getValue().asDouble();
            // BINARYの場合  
            byte[] byteValue = row.getLatestColumn("BinaryAttributeName").getValue().asBinary();
            // BOOLEANの場合  
            Boolean booleanValue = row.getLatestColumn("BooleanAttributeName").getValue().asBoolean();

 
またこの時、テーブル定義に沿った型ではないメソッドを使用した場合「IllegalStateException」が発生します。

GetRangeApi

GetRangeは取得開始行と終了行を指定して、その間にある行を取得するAPIです。

GetRangeの仕様に関しては、前回のブログにて記載しておりますので参照をお願いしします。

以下のサンプルコードは、全範囲取得のサンプルです。

    /**
     * データの範囲取得.
     * 
     * @param client TableStoreのApiClient
     * @param tableName テーブル名
     */
    public void sampleGetRange(SyncClient client,  String tableName) {
        // Criteria生成
        RangeRowQueryCriteria criteria = new RangeRowQueryCriteria(tableName);

        // 最小行PK・最大行PKのインスタンス宣言
        PrimaryKeyBuilder minBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
        PrimaryKeyBuilder maxBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();

        // ① PK設定
        minBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.INF_MIN);
        maxBuilder.addPrimaryKeyColumn("Time", PrimaryKeyValue.INF_MAX);

        minBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.INF_MIN);
        maxBuilder.addPrimaryKeyColumn("Prefecture", PrimaryKeyValue.INF_MAX);

        // ② 昇順の場合
        criteria.setInclusiveStartPrimaryKey(minBuilder.build());
        criteria.setExclusiveEndPrimaryKey(maxBuilder.build());

        criteria.setMaxVersions(1);

        while (true) {
            // ③ レコード取得
            GetRangeResponse response = client.getRange(new GetRangeRequest(criteria));
            for (Row row : response.getRows()) {
                Map<String, String> dataMap = new LinkedHashMap<>();
                for (Map.Entry<String, PrimaryKeyColumn> primaryKey : row.getPrimaryKey().getPrimaryKeyColumnsMap().entrySet()) {
                    dataMap.put(primaryKey.getKey(), primaryKey.getValue().getValue().toString());
                }

                for (Column column : row.getColumns()) {
                    dataMap.put(column.getName(), column.getValue().toString());
                }

                System.out.println(dataMap);
            }

            // ④ 一度のリクエストで取得制限となった場合は次のプライマリーキーから取得する
            if (response.getNextStartPrimaryKey() != null) {
                criteria.setInclusiveStartPrimaryKey(response.getNextStartPrimaryKey());
            } else {
                break;
            }
        }
    }

 
① PK設定
取得する範囲の最小行と最大行のPKの値を設定します。
上記例の場合は、全範囲を取得する場合です。
「PrimaryKeyValue.INF_MIN」は最小値
「PrimaryKeyValue.INF_MAX」は最大値 を意味します。

設定する場合は、必ず「最小行 < 最大行」となるようにしてください。

そのほかのPK設定での注意点はこれまでのAPIと同様です。

② 昇順・降順指定
取得時の順番を指定します。
上記例の場合は昇順での場合で、InclusiveStartPrimaryKeyに最小行をExclusiveEndPrimaryKeyに最大行を入れます。
降順にしたい場合は以下のようにします。

        // InclusiveStartPrimaryKeyに最大行をExclusiveEndPrimaryKeyに最小行
        criteria.setInclusiveStartPrimaryKey(maxBuilder.build());
        criteria.setExclusiveEndPrimaryKey(minBuilder.build());
        
        // 降順にする指定をします。(デフォルトが昇順なので、昇順の場合は不要)
        criteria.setDirection(Direction.BACKWARD);

 
③ レコード取得
APIを実行してレコード取得します。
「response.getRows()」で取得した行のリストを取得できます。
パース時の注意点はGetRowと同じです。

④ 取得上限時の再リクエスト
GetRangeは1度に取得できるデータ量の上限があります。
取得上限に達した場合、レスポンス情報に次の行のPK情報が含まれて返却されますので、 InclusiveStartPrimaryKeyにその情報を入れ、再度リクエストを投げる必要があります。

注意する点としては、リクエストで取得数を制限する場合です。
リクエスト時に以下のようにLimitで取得数を設定できるのですが、この設定を行っても1度に10行取るだけで、「次の行のPK情報(11行目)」が含まれて返ってきます。
そのため、④ のコードを書いてしまうと結果的に範囲内すべての行を複数回に分けて取得してしまうことになってしまうので注意しましょう。

         criteria.setLimit(10);

読み取りAPIのリクエストオプションとコスト面に関する注意点

① フィルター
GetRangeを使用する際はフィルターと呼ばれる機能を用いることができます。
例えば、以下のようにフィルターを作成した場合「Precipitation=0」のデータのみ取得することができます。

        SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter("Precipitation",
                SingleColumnValueFilter.CompareOperator.EQUAL, ColumnValue.fromLong(0));

        criteria.setFilter(singleColumnValueFilter);

 
ただし、このフィルターは範囲内のデータをすべて取得した後にバックエンドサーバ側でフィルタ処理をし、データを返す仕組みとなっています。
そのため、読み取りコストとしては範囲内のデータをすべてにかかってしまう点に注意してください。

取得頻度もしくは取得範囲が少ないクエリの場合にはFilterを用いたGetRangeでの取得でもよいですが、そうでなければ前回のブログにて記載しているインデックス機能を利用した方法での取得を推奨します。

詳細の確認や、複数列で指定したい場合は以下のドキュメントを参照してください。

https://www.alibabacloud.com/help/doc-detail/43029.htm

② 取得属性列の選択
GetRowやGetRangeを使用する際は取得する属性列を指定することができます。(PKは必ず取得される)
アプリケーション側であらかじめ使用する属性が限られている場合に、この機能を用いることで、読み込み時のデータサイズを少なくすることができ読み取りコストを抑えることができます。

        List<String> getColumns = new ArrayList<String>(){
            {
                add("Precipitation");
                add("WindDirection");
            }
        };

        criteria.addColumnsToGet(getColumns);

おわりに

今回は、TableStoreのデータ取得ツール紹介とAPIを使用する際の注意点を紹介しました。

TableStoreを使用したアプリケーションを作成する際に役に立てていただければ幸いです。