このエントリーは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のデータを別のアプリケーションにリストアする機能もあるので、書き込みをブロックして問題ないようであれば、
- Datastoreへの書き込みをブロック
- Datastoreのデータをバックアップ
- 移行用アプリケーションのDatastoreヘリストア
- 移行を実施
と言った流れでも対応できるかと思います。
しかしながら、諸般の事情により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()); }
開発環境を構築する
実はここが一番の難題だったりします。
今回は既にデータがあってそれを移行するパターンです。
- ローカル環境のSlim3アプリケーションとかでデータを登録
- Slim3アプリケーションのlocal_db.binをDatastore API用のアプリケーションにコピー
- 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だったので、
- Slim3の方のSDKバージョンを1.9.0に変更
- http://d.hatena.ne.jp/t-horikiri/20120605/1338915771 の問題があるため、Slim3のバージョンを1.0.16に変更
- 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を使いたい場合は、アプリ名を揃えておいた上で
/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(); } }