データを引っこ抜いてDatastoreから決別する方法

このエントリーはGoogle Cloud Platform Advent Calendar 2014の9日目です。

最近はBigQueryの方が話題になっておりますね。

昔Datastoreベースで作ったアプリケーションをBigQueryにコンバートし直したい、いやむしろデータをRDBSにコンバードしてGAEから撤退したいという方向けに!?Low Level APIを使ってDatastoreにアクセスする方法をご紹介します。

Datastoreにアクセスするパターン

DatastoreとRDBS両方にアクセスしてデータのコンバートを行う場合、以下のパターンがあるかと思います。

  • GAE上のアプリケーションを使用してデータのコンバートを行う
  • 外部からDatastoreにアクセスしてデータのコンバートを行う

GAE上のアプリケーションを使用してデータのコンバートを行う

もしSlim3で作ったアプリケーションなどでDatastoreにアクセスしていて、コンバート用のプログラムをその中に差し込めるのであれば、Slim3を使うのが一番楽だと思います。

Datastoreのデータを別のアプリケーションにリストアする機能もあるので、書き込みをブロックして問題ないようであれば、

  1. Datastoreへの書き込みをブロック
  2. Datastoreのデータをバックアップ
  3. 移行用アプリケーションのDatastoreヘリストア
  4. 移行を実施

と言った流れでも対応できるかと思います。

しかしながら、諸般の事情によりGAE上のアプリケーションを触れない場合には、外部からDatastoreにアクセスしてデータのコンバートを行うしか方法が残されていません。

今回は外部からDatastoreにアクセスする方法をご紹介します。

その際には何点か辛みを乗り越えなければいけないポイントがあるので、そこをまずは解説いたします。

アプリケーションの設定

以下のURLに記載されている手順で外部からDatastoreにアクセスできるためのAPI有効化、アカウント作成を行います。

Accessing an existing App Engine Datastore from another platform

https://cloud.google.com/datastore/docs/activate

環境変数を使わない形でDatastore接続情報を読み込む

JavaからDatastoreにアクセスするときに紹介されている方法では、環境変数に値をセットするように記載されています。

https://cloud.google.com/datastore/docs/getstarted/start_java/

export DATASTORE_SERVICE_ACCOUNT=<service-account>
export DATASTORE_PRIVATE_KEY_FILE=<path-to-private-key-file>

でも正直コレはイケてないので、プログラム内で指定をするようにします。

  public static final String ACCOUNT = "XXXXXX@developer.gserviceaccount.com";
  public static final String DATASET_ID = "APP_ID";
  public static final String PRIVATE_KEY_FILE = "/path/to/XXX.p12"
  public static final String DATASTORE_HOST = "localhost";
  private static Datastore datastore;

  public static void connectDatastore() throws GeneralSecurityException,
      IOException {

    DatastoreOptions.Builder optionsfromEnv =
      DatastoreHelper.getOptionsfromEnv();

    Credential newCredential =
      DatastoreHelper.getServiceAccountCredential(ACCOUNT, PRIVATE_KEY_FILE);

    optionsfromEnv.credential(newCredential);

    if (!StringUtils.isEmpty(DATASTORE_HOST)) {
      optionsfromEnv.host(DATASTORE_HOST);
    }

    datastore =
      DatastoreFactory.get().create(optionsfromEnv.dataset(DATASET_ID).build());

  }

開発環境を構築する

実はここが一番の難題だったりします。

今回は既にデータがあってそれを移行するパターンです。

  1. ローカル環境のSlim3アプリケーションとかでデータを登録
  2. Slim3アプリケーションのlocal_db.binをDatastore API用のアプリケーションにコピー
  3. Datastore API用のアプリケーションを起動

という感じでDatastoreにデータを入れた状態にして対応します。

localhostでDatastore APIサーバを立てる方法

https://cloud.google.com/datastore/docs/tools/devserver を参考に構築します。

ダウンロード

https://cloud.google.com/datastore/docs/downloads から gcd-v1beta2-rev1-2.1.1.zip をダウンロードして解凍します

アプリの作成

コマンドプロンプトで以下を実行する

cd /path/to/gcd-v1beta2-rev1-2.1.1
gcd.cmd create /path/to/app

起動

起動します。

gcd.cmd start /path/to/app

起動時にポートを指定するなどのオプションがあります。

辛みの理由

どうやらlocalhostでDatastore APIを起動するときにはテスト用?のモードで起動して、アクセス権限の設定を回避しているようです。そのため、ローカル環境のSlim3 のweb.xmlを編集して直接アクセスすれば便利では、という事はできないみたいです。

実際に以下の手順で試してみました。

Datastore APIのサーバのappengine-java-sdkが1.9.0だったので、

  1. Slim3の方のSDKバージョンを1.9.0に変更
  2. http://d.hatena.ne.jp/t-horikiri/20120605/1338915771 の問題があるため、Slim3のバージョンを1.0.16に変更
  3. web.xmlに/datastore のアクセスパターンを追記

web.xml追記内容

  <security-constraint>
    <web-resource-collection>
      <url-pattern>/datastore/*</url-pattern>
    </web-resource-collection>
    <user-data-constraint>
      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
  </security-constraint>

  <servlet>
    <servlet-name>DatastoreApiServlet</servlet-name>
    <servlet-class>
      com.google.apphosting.client.datastoreservice.app.DatastoreApiServlet
    </servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>DatastoreApiServlet</servlet-name>
    <url-pattern>/datastore/*</url-pattern>
  </servlet-mapping>

とかやってみましたが、

com.google.apphosting.client.serviceapp.BaseApiServlet writeErrorResponse
重大: PERMISSION_DENIED: Unauthorized.

のエラーがでて無理でした。。。

おそらくテストコード書いてユニットテストする分には、アクセスできるんじゃないかと思います。。。

Slim3のlocal_db.binを使いたい場合は、アプリ名を揃えておいた上で

/war/WEB-INF/appengine-generated/local_db.bin

/path/to/app/WEB-INF/appengine-generated/ にコピーします。

最後につらみを乗り越えた後の各アクセス用メソッドを列挙して終わりにします。

Datastoreから全件を取得する

static List<Entity> runQuery(Query.Builder query) throws DatastoreException {
    RunQueryRequest.Builder request = RunQueryRequest.newBuilder();
    request.setQuery(query.build());
    RunQueryResponse response = datastore.runQuery(request.build());
    List<Entity> entities = new ArrayList<Entity>();

    List<EntityResult> results = null;
    while (response != null) {
      results = response.getBatch().getEntityResultList();
      for (EntityResult result : results) {
        entities.add(result.getEntity());
      }
      if (response.getBatch().getMoreResults() == QueryResultBatch.MoreResultsType.NOT_FINISHED) {
        ByteString endCursor = response.getBatch().getEndCursor();
        query.setStartCursor(endCursor);
        response = datastore.runQuery(request.build());
      } else {
        response = null;
      }
    }

    return entities;
  }

Select

  public static List<Entity> getLists(String hoge) throws DatastoreException {

    Query.Builder query = Query.newBuilder();
    query.addKindBuilder().setName("Hoge");
    query.setFilter(DatastoreHelper.makeFilter(
      "Co",
      PropertyFilter.Operator.EQUAL,
      DatastoreHelper.makeValue(hoge)));

    return runQuery(query);
  }

Keyで検索する場合には

    query.setFilter(DatastoreHelper.makeFilter(
      "__key__",
      PropertyFilter.Operator.HAS_ANCESTOR,
      DatastoreHelper.makeValue(key)));      

のように指定します。

Insert

 private static void createData(String hoge) throws DatastoreException {

    BeginTransactionRequest.Builder treq = BeginTransactionRequest.newBuilder();
    BeginTransactionResponse tres = datastore.beginTransaction(treq.build());
    ByteString tx = tres.getTransaction();

    CommitRequest.Builder request = CommitRequest.newBuilder();
    request.setTransaction(tx);
    Entity.Builder entity = Entity.newBuilder();

    Key.Builder key =
      Key.newBuilder().addPathElement(
        Key.PathElement.newBuilder().setKind("Hoge").setName(hoge));
    entity.setKey(key);
    entity.addProperty(Property.newBuilder().setName("v").setValue(
      Value.newBuilder().setIntegerValue(1)));
    entity.addProperty(Property.newBuilder().setName("sV").setValue(
      Value.newBuilder().setIntegerValue(1)));

    request.getMutationBuilder().addInsert(entity);
    CommitResponse response = datastore.commit(request.build());
    request.clearTransaction();
  }

Update

  public static void updateData(Entity model, String hoge)
      throws DatastoreException {

    BeginTransactionRequest.Builder treq = BeginTransactionRequest.newBuilder();
    BeginTransactionResponse tres = datastore.beginTransaction(treq.build());
    ByteString tx = tres.getTransaction();

    CommitRequest.Builder request = CommitRequest.newBuilder();
    request.setTransaction(tx);
    Entity.Builder entity = Entity.newBuilder(model);
    entity.addProperty(Property.newBuilder().setName("Hoge").setValue(
      Value.newBuilder().setStringValue(hoge)));
    List<Property> propertyList = entity.getPropertyList();
    for (int i = 0; i < propertyList.size(); i++) {
      String name = propertyList.get(i).getName();
      if ("sV".equals(name)) {
        entity.setProperty(i, Property.newBuilder().setName("sV").setValue(
          Value.newBuilder().setIntegerValue(2)));
      }
    }

    request.getMutationBuilder().addUpdate(entity);
    CommitResponse response = datastore.commit(request.build());
    request.clearTransaction();
  }

Delete

  private static void deleteData(Entity model) throws DatastoreException {
      BeginTransactionRequest.Builder treq =
        BeginTransactionRequest.newBuilder();
      BeginTransactionResponse tres = datastore.beginTransaction(treq.build());
      ByteString tx = tres.getTransaction();

      CommitRequest.Builder request = CommitRequest.newBuilder();
      request.setTransaction(tx);
      Entity.Builder entity = Entity.newBuilder(model);
      request.getMutationBuilder().addDelete(entity.getKey());
      CommitResponse response = datastore.commit(request.build());
      request.clearTransaction();
    }
  }