Download - appengine java night #1
appengine java night #1実開発前に知っておきたいLow-Level APIと自動テスト
# Low-Level API かわいいよ!
shin1ogawa@株式会社トップゲート
自己紹介...shin1ogawaです!Google, Java, AppEngine, Eclipse, Wicket, Maven, Hudson, 構成管理... • 参加しているOSSプロダクトやコミュニティ
• [SF.jp]Jiemamy...データベースの進化的設計
• [SF.jp]gae-j-samples...GAE/Jのサンプル、GAE/J/Maven統合プラグイン
• [SF.jp]gaejtools...GAE/J用のユーティリティ(予定)
• [SF.jp]asclipse...Amateras AIR GEAR用のAS3構文解析ライブラリ
• java-ja, wicket-ja,
株式会社トップゲート(http://www.topgate.co.jp)で、GoogleAppEngineやGoogleAppsを使用したソリューションを提供する為のお仕事をしています。
アジェンダ
1.Low-Level API1.1.概要とか特徴とか1.2.実行時の仕組み1.3.DatastoreService
2.自動テスト2.1.AppEngineの実行環境について
3.おまけ
Low-Level API概要とか特徴とか
High-Level API(造語)の罠高レベルなインターフェース)よりも各サービスが持つ機能の方がはるかに小さいため、高レベルなインターフェースだけを見て学習すると、「この機能は実装されていない」という事態がたびたび発生する。
高レベルAPI(機能多)>>>>>実装(機能少)他にも、各サービスの機能の実装と高レベルAPIの間の概念の違いを無理に解決しようとしたインターフェースによって、本来の実装が見えにくくなったりもする。これらは特にDatastoreサービスの時に顕著に見られる。
Low-Level APIとは?
AppEngineから色々なサービスを利用できるが、各サービスへのアクセス方法は2種類提供されている事が多い。例えば、データストアへのアクセスにはJDO/JPAというインターフェースが提供されており、それとは別にLow-Level APIと呼ばれるインターフェースも提供されている。これらのうち
開発者にとってフレンドリーではない方がLow-Level API(低レベルAPI)と呼ばれる。
Low-Level APIは使いにくい?
そーでもない。AppEngineから使用できる各サービスへのインターフェースは本当はかなり機能が少ない。その本来の少ない機能へのアクセスだけを提供しているためLow-Level APIは
飾らない、シンプルな、素直なヤツというのが真実。薄くシンプルだからオレオレFrameworkを作る楽しみも増すし、作りやすい。
※正確には、逆にLowLevelAPIの方が機能が少なかったり…?
Highなの?Lowなの?(建前)
やっぱりJDO等の高レベルなAPIは便利だし、一番良いのは両方を理解しておく事。ただしshin1ogawa個人的には、まずLow-Level APIを知る事で各サービスの実態を知ってから高レベルAPIに進む、という道がオススメ。両方理解してから、両者を吟味して選択したり、使い分けたりするのが良いんじゃないか、と思います。
Highなの?Lowなの?(本音)• 覚える事がとにかく少ないので、飼いならしやすい。• サービスの実装へアクセスするために、サービスに忠実なインターフェースで操作を要求してくるので、サービスの実装へのインターフェースが垣間見えたりしてマニアックな興味をそそられる。
• 後で説明する、ApiProxy#DelegateのmakeSyncCall()をほげるためにはLow-Level APIが必要。
• Low-Level APIの方が親切だったりする事も稀にある。
Low-Level APIメインでいいんじゃない?
Low-Level APIの弱点飾りっ気が無い、シンプルで、素直で、マニア心がソソられるLow-Level APIだが、APIとは関係ない弱点が…。
「公式ドキュメントには大した説明が無い」チュートリアルも無い。学習や調査はJavadocの情報のみ。しかしこのドキュメントにも罠があり、日本語サイトを見ると古いAPI情報だったりするという状況。当然ソースも提供されていない。だけど…
マニアック度が増して燃えるよね!shin1ogawaが簡単にまとめた情報も参考にどうぞ!
https://sites.google.com/a/topgate.co.jp/systemsolution/Home/googleappengine/datastore-lowlevelapi
Low-Level API実行時の仕組み
Low-Level API実行の仕組み1.アプリケーションがXXXService#hogeMethod()を実行する
2.XXXServiceの実装が、ApiProxy#getDelegate()でDelegateのインスタンスを取得する。
3.XXXServiceの実装が、受け取ったパラメータをProtocolBufferオブジェクトの実装(byte[]に変換できる)として組み立て、Delgate#makeSyncCall( String serviceName, String methodName, byte[] request)をリフレクションで実行する。
Low-Level API実行の仕組み4.DelegateのインスタンスがserviceName, methodNameを元に実行すべきサービスの実装とメソッドを特定する。
5.呼び出すメソッドの引数の型 (ProtocolBufferオブジェクトの実装) を取得し、byte[] requestからその実装へ組み立て、サービスのスタブの実装を実行する。
6.サービスのスタブの実装がサービスノード群への通信が行われる(?)
Low-Level API実行の仕組み7.サービスのスタブの実装は実行結果をProtocolBufferオブジェクトとして組み立て、byte[] responseに変換してDelegateのインスタンスに返す。
8.Delegateのインスタンスがbyte[] responseをProtocolBufferオブジェクトとして組み立て直し、XXXServiceの実装へ返す。
9.XXXServiceの実装がProtocolBufferオブジェクトをJavaのオブジェクトに変換し、アプリケーションへ返す。
Low-Level API実行の仕組み
1.DatastoreService#put(Entity entity)1.引数であるentityからDatastorePb.PutRequest requestPbを作成
2.byte[] ApiProxy#getDelegate( “datastore_v3”, “PUT”, requestPb.toByteArray())を実行
3.返り値のbyteからDatastorePb.PutResponse responsePbを作成
4.responsePb.getKey(0)をアプリケーションに返す
例
Low-Level APIDatastoreService
使い方
Datastore Service重要なクラス• Key, KeyFactory• Entity• DatastoreService• Transaction• Query• FilterOperator/SortDirection• PreparedQuery• FetchOptions
Datastore ServiceKeyエンティティのKeyは「ApplicationID, Kind, 親EntityのKey(ancestorKeyと呼ぶ)」を保持している。
RootEntityのKeyは親EntityのKeyを持っていない。
KeyのみでEntityGroupが構成されるKey以外の属性でOwnedだとかUnonwedだとか、そんなものはEntityGroupには一切関係が無い。
KeyのみでEntityGroupが構成される
Datastore ServiceKey
Parent(1) prop1 prop2 children
Child(1) prop1 prop2
Child(2) prop1 prop2
Child(3) prop1 prop2}
Datastore ServiceKey
Parent(1) prop1 prop2 children
Child(1) prop1 prop2
Child(2) prop1 prop2
Child(3) prop1 prop2}
Datastore ServiceKey
Parent(1) prop1 prop2
Parent(1)/Child(1) prop1 prop2
Parent(1)/Child(2) prop1 prop2
Parent(1)/Child(3) prop1 prop2
KeyのみでEntityGroupが構成される
Datastore ServiceKey
Parent(1) prop1 prop2
Parent(1)/Parent(2) prop3 prop4
Parent(1)/Parent(3) prop1 prop3
Parent(1)/Parent(4) prop2 prop4
同じKind内でEntityGroupを構成、も可能
Datastore ServiceKey, KeyFactory• Key#getKind()でKind名を取得できる。• Key#getParent()で親EntityのKeyを取得できる。 • 実態は単なる文字列• JDOでいう”encoded-pkey”とかの値の事。• KeyFactory#keyToString(Key)でKeyから文字列に変換。
• KeyFactory#stringToKey(String)でその逆。
Datastore ServiceKey, KeyFactory• KeyFactory#createKey(String kind, Stirng name)で生成できる。• Key#getName()で生成に使用したString nameを取得できる。
• 1.2.5から数字で始まるnameも指定できるようになった。• Keyを指定せずに保存すると自動採番された値で生成される• 1.2.5からは保存前にあらかじめ採番したKeyを取得できる• Key#getId()で自動採番されたLong idを取得できる。• id値はKind内やアプリケーション内で一意なワケではない。
Datastore ServiceEntity• 名前の通り、エンティティを保持するクラス。• JDO/JPAと違い、Pojoな型を用意するわけではない。• Entity#getKey()で主キーを取得できる。• getKey()で取得できる値に対するアダプタ• Key Entity#getParent()• String Entity#getKind()
Datastore ServiceEntity• 属性の値はMapのように 属性名=値 で保持される。• setProperty(String 属性名, Obejct 値)• setUnindexedProperty(属性名, 値)• setUnindexedProperty(“p”, String|Long|...)すると、プリミティブな値を保持する属性でもindexを作成せずに保持できたりする。
JDOと違い、Entityごとに違う属性のセットを保持する事が可能"スキーマレス
Datastore ServiceEntity• getProperty(String 属性名)• Object型の属性値が返される"常にcast• プリミティブな型以外は基本的にはシリアライズして保持するしか無い。
• 格納された属性値の注意点• Integerを格納する"実際はLongで格納/取得される• Setを格納する"実際はArrayListで格納/取得される
Datastore ServiceEntity• hasProperty(String 属性名)• 指定された属性名の属性を持つか?を返す。• 存在する属性名でnull値(viewerでの”<null>”)• getProperty() " null• hasProperty() " true
• 存在しない属性(viewerでの”<missing>”)• getProperty() " null• hasProperty() " false
• スキーマレスだから重要!
Datastore ServiceEntity• コンストラクタ• new Entity(kind名)• new Entity(kind名, 親Key)• new Entity(kind名, key生成用のname値)• new Entity(kind名, key生成用のname値, 親Key)• new Entity(Key primaryKey) ... 1.2.5で追加
• 定数• KEY_RESERVED_PROPERTY ... “__key__”• PrimaryKeyを意味する属性名
Datastore ServiceDatastoreService• 他のサービスと同様にFactoryから取得する• DatastoreServiceFactory#getDatastoreService()
• KeyによるEntityの取得• Entity get([Transaction tx,] Key key)• Map<Key, Entity> get(
[Transaction tx,] Iterable<Key> key)• 存在しないKeyだった場合は
EntityNotFoundException が投げられる。• QueryによるEntityの取得• "後述(Queryの項で説明)
Datastore ServiceDatastoreService• Entityの保存• Key put([Transaction tx,] Entity entity)• List<Key> put(
[Transaction tx], Iterable<Entity> entity)• 汎用の型で操作するので、別のKindをまとめて…とか、親子関係にあるEntityを一気に更新する…とかでもおk。
• Entityの削除• delete([Transaction tx,] Key... keys)• delete([Transaction tx,] Iterable<Key> keys)• Keyだけを使う。
Datastore ServiceDatastoreService• 自動採番されるKeyをあらかじめ取得できる ...1.2.5で追加
• KeyRange allocateIds( [Key parentKey,] String kind, long num)
• KeyRange• Key getStart()• Key getEnd()• Iterator<Key> iterator()
• Transaction• Transaction beginTransaction()• Transcation getCurrentTransaction()• 他にもいくつか。
Datastore Service保存のサンプル(Entityの準備)KeyRange parentKeys = service.allocateIds("Parent", 1);Key parentKey = parentKeys.getStart();KeyRange childKeys = service.allocateIds(parentKey, "Child", 2);Iterator<Key> childKeysIterator = childKeys.iterator();Entity parent = new Entity(parentKey);Entity child1 = new Entity(childKeys.next());Entity child2 = new Entity(childKeys.next());
Datastore Service保存のサンプル(保存)
Transaction tx = service.beginTransaction();try { List<Entity> entities = Arrays.asList(parent, child1, child2 service.put(tx, entities); tx.commit();} finally { if (tx.isActive()) { tx.rollback(); }}
Datastore ServiceQuery• new Query(String kind)• JDOっぽく、フィルタ条件やソート条件を指定する場合
• new Query(String kind, Key ancestorKey)• 指定したKindで、指定した親キーに属するエンティティを取得する。
• new Query(Key ancestorKey)• 指定した親キーに属するエンティティを、すべてのKindにまたがって、末端のエンティティまで取得する。• ローカル環境では動作しない、という不具合が残念!
Datastore ServiceQuery• フィルタ条件の指定• Query addFilter(String propertyName,
FilterOperator operator, Object value)• ソート条件の指定• Query addSort(String propertyName,
SortDirection direction)• 取得対象をキーのみにしぼる• Query setKeysOnly()• Keyにしかアクセスしないのでずいぶん早くなる。
主キーを意味する属性名は Entity.KEY_RESERVED_PROPERTY
Datastore ServicePreparedQuery• DatastoreService#prepare()メソッドにQueryオブジェクトを渡す事で、PreaparedQueryが取得できる。
• 件数を取得する• int countEntities()
• 一件だけ取得する• Entity asSingleResult()• 条件にマッチするエンティティが無ければnullが返る。• 複数のEntityが条件にマッチした場合は
TooManyResultsExceptionが投げられる。
Datastore ServicePreparedQuery• 複数件を取得する• List<Entity> asList(FetchOptions)• まとめてフェッチする事ができる。
• Iterator<Entity> asIterator([FetchOptions])• いわゆるカーソルのような操作になる。
• Iterable<Entity> asIterable([FetchOptions])• iterator()を取得した瞬間にDatastoreにアクセスする。そこから先はasIterator()と同じ動作。
• FetchOptions• フェッチのためのオプション。offset, limit等。
Datastore ServiceQueryのサンプルQuery query = new Query(“Entity”);query.addFilter(“name”, FilterOperator.GREATER_THAN_OR_EQUAL, “hoge”) .addSort("name", SortDirection.ASCENDING) .addSort("__key__", SortDirection.ASCENDING);
DatastoreService service = DatastoreServiceFactory.getDatastoreService();List<Entity> entities = service.prepare(query).asList( FetchOptions.Builder.withOffset(0).limit(100));
Datastore ServiceQueryのサンプル: EntityGroupの取得Iterator<Entity> entityGroup = service.prepare(new Query(rootKey) .addSort(“__key__”)).asIterator( FetchOptions.Builder.withOffset(0).limit(100));Parent parent = null;while (entityGroup.hasNext) { Entity entity = entityGroup.next(); if (entity.getKind().equals(“Parent”)) parent = EntityUtil.toBean(entity,Parent.class); else if (entity.getKind().equals(“Child”)) parent.getChidren().add( EntityUtil.toBean(entity, Child.class); ...
Datastore ServiceちょっとJDOに話を戻しますJDOでたまに話題になる話についてちょっと考察。
class MyEntity { List<Child> children1; List<Child> children2;}myEntity.getChildren1().addAll(childA, childB);myEntity.getChildren2().addAll(childC, childD);manager.makePersistent(myEntity);
上記のように、同じ型の別のListを属性として持っているエンティティがあったとした時。
Datastore ServiceちょっとJDOに話を戻しますMyEntity myEntity = manager.getObjectById(MyEntity.class, key);List<Child> children1 = myEntity.getChildren1();List<Child> children2 = myEntity.getChildren2();
この時のchildren1には、childAとchildBだけではなく、childCとchildDが格納されている。children2はnull。
Datastore ServiceちょっとJDOに話を戻します
MyEntiy(1)
MyEntiy(1)/Child(A)
MyEntiy(1)/Child(B)
MyEntiy(1)/Child(C)
MyEntiy(1)/Child(D)
当然、親子関係は正しい構成で保存されているが、Child(A-D)がそれぞれどの属性に保持されていたか?は知りようが無い!
このように、実装を意識する事でハマリは遭遇しにくかったり、ハマった時も理解できる。
自動テスト環境について
自動テスト環境AppEngine環境の仕組みローカル環境だけではなく、AppEngineの各環境はすべて
ApiProxy#getDelegate()で取得されるDelegateオブジェクト経由で実行される。デプロイ環境ではWebコンテナがDelegateオブジェクトを設定するし、ローカルでWebコンテナを起動したときも同じ。また、
ApiProxy#getCurrentEnvironment()
で取得されるEnvironmentオブジェクトから環境情報(ApplicationID, versionID等)を取得できる必要もある。
自動テスト環境手動でAppEngine環境の準備をする
JUnit等、SDKのWebコンテナを使用しない環境ではそれらのオブジェクトを生成してApiProyに設定してやる必要がある。• Environmentのは独自に実装し、ApiProxy#setEnvironmentForCurrentThread()で設定する。
• Delegateの実装としては ApiProxyLocalImpl クラスを生成してApiProxy#setDelegate()で設定する。
自動テスト環境Environment
•スレッドごとに存在する•ローカルではSingleThreadで動作するので、常にひとつだけ。•ローカル環境でマルチスレッドで動作させるといろいろと問題が発生するはず
• ApplicationID, versionID, 認証済みか?認証済みならそのアカウントは?管理者権限を持っているか?等を返す必要がある。
自動テスト環境ApiProxyLocalImpl(Delegateの実装)
• Delegateの実装というよりは、ローカル環境用のサービスのスタブへの接続を行うためのアダプタで、Delegateよりも随分大きい実装を含んでいる。
•コンストラクタに指定したフォルダをルートフォルダとしてサービスのスタブを初期化する。•指定したフォルダ配下にWEB-INF/queue.xmlやdatastore-indexes.xmlを用意しておくと、サービスのスタブ開始時にそれを読み込んでくれる。
自動テスト環境Delegateを実装する• ApiProxyLocalImplを使用しなければサービスのスタブを使用できない。が、これを継承するかこれへ処理を委譲するDelegateを自前で実装する事は可能。•継承すると…ApiProxyLocalImplを必要とするのでデプロイ環境では使用できない。
• ApiProxy#getDelegate()を抽象的にApiProxyインターフェースとして取得し、それへ委譲するDelegateの実装をApiProxyに設定すれば、各サービスへのアクセスをフックする事が可能。AOPっぽいカンジ。
自動テスト環境Delegateを実装するclass MyDelegate implements Delegate { Delegate original = ApiProxy.getDelegate(); public byte[] makeSyncCall( Environment environment, String service, String method, byte[] request) throws ApiProxyException { System.out.println(service+”:”+method); return original.makeSyncCall( environment, service, method, requst); }}
おまけ1
おまけ1makeSyncCall()をほげってみる?
byte[] Delegate#makeSyncCall( Environment environment, String service, String method, byte[] request) throws ApiProxyException
おまけ2makeSyncCall()をほげってみる?
byte[] Delegate#makeSyncCall( Environment environment, String service, String method, byte[] request) throws ApiProxyException
おまけ2makeSyncCall()をほげってみる?•例えば datastore_v3#GET の場合•送信時: DatastorePb.GetRequest• new GetRequest().mergeFrom(byte[])• GetRequest#keyIterator()で引数のKeyのIteratorを取得できる。
•受信時: DatastorePb.GetResponse• new GetResponse().mergeFrom(byte[])• GetResponse#entityIterator()で返り値のEntityのIteratorを取得できる。
おまけ2makeSyncCall()をほげってみる?•これらを利用して、以下のようなDelegateを作成・適用できる…はず(まだ実際に試した事は無いんですが)• datastore_v3#GETをフックする• memcache#GETをして、存在すればdatastoreへはアクセスせずにmamcache#GETの結果を返す
• datastore_v3#PUT/DELETEをフックする• memcache#DELETEをしてからdatastoreへアクセする
おまけ2
おまけ2com.google.apphosting.utils.remoteapi.RemoteApiServlet• Googleアカウントのadmin権限を持つが必須• HttpServletの実装でGETとPOSTが実装されている。• GETは何に使うかわからないが、上記の条件で認証済みの時はyamlのフォーマットで適当な値が返される。
•実際にAPIを実行する際はPOSTを使用する。• POST時は常にHttpヘッダに“X-appcfg-api-version”パラメータを設定する必要があるようだ。
おまけ2com.google.apphosting.utils.remoteapi.RemoteApiPb
• RemoteApiServlet経由でAPIを実行する際にPOSTするためのProtocolBufferの実装クラスが定義されている• Requestクラス• TransactionRequestクラス• Responseクラス• ApplicationErrorクラス
おまけ2
•ローカル環境でDatastoreにふつーにアクセスするだけで、RemoteApiDelegateがRemoteApiServletへアクセスしてデプロイ環境のデータストアを操作するとか。
• Eclipse Plug-inからデプロイ環境のデータストアの内容をグラフィカルに覗いたりとか。
•ローカルのCUIからデプロイ環境のMemcacheのstatsを確認したり操作したりとか。
夢が広がりますね...!!!
ご清聴ありがとうございました!
shin1ogawa@株式会社トップゲート