2011年12月18日日曜日

Storyboardでテーブルビューのセルをカスタマイズする (iOS)

StoryboardではTableViewのセルのカスタマイズも1画面でできるようになりました。

セルのプロトタイプから動的にセルを生成する

カスタムセルのプロトタイプを作って、プログラムで動的にセルの内容を決定するTableViewを作ってみます。

最初に、プロジェクトを作るところから始めます。Master Detail Applicationテンプレートから新規プロジェクトを作ります。

2011年12月13日火曜日

Storyboardで画面遷移を作る (iOS)

Xcode4.2からStoryboadというものが使えるようになりました。いままで画面ごとにnibファイルを作ってプログラムで画面遷移することができましたが、Storyboardを使えば各画面の関係を1画面で見渡すことができます。また、単純な画面遷移ならプログラムを一切書かずに作ることができます。

最も簡単な画面遷移

ボタンを押したら次の画面に移動するだけの簡単な画面遷移を作ってみます。

まずはプロジェクトの作成から。

File –> New –> New Project でSingle View Applicationを選択します。
新規プロジェクトのプロジェクト名などを入力する画面で「Use Storyboard」のチェックを入れて、プロジェクトを作成します。

2011年9月26日月曜日

Wi-Fiでゲートウェイにpingする (Android)

AndroidのWi-Fiを使ってpingする方法です。

まずはゲートウェイのアドレスを調べます。DhcpInfoのgatewayフィールドに入っています。

WiFiManager wifi = (WifiManager)getSystemService(WIFI_SERVICE);
DhcpInfo dhcpInfo = wifi.getDhcpInfp();
int gateway = dhcpInfo.gateway;

ここにはリトルエンディアンで入っています。これをビッグエンディアンの配列にします。

byte[] gatewayAddr = new byte[] {
        (byte)(gateway&255),
        (byte)((gateway>>8)&255),
        (byte)((gateway>>16)&255),
        (byte)((gateway>>24)&255)
};

あとは普通のJavaプログラムと同様にpingします。

InetAddress addr = InetAddress.getByAddress(gatewayAddr);
if ( addr.isReachable(3000) ) {
    …
}

2011年7月7日木曜日

GalaxySを2.3.3にアップデートしたらバッテリー消費が激しくなった

GalaxyS Android2.3.3のバッテリー問題。突然バッテリーの消費が激しくなり、8時間くらいでバッテリーを完全消費してしまう。

この問題、どうやら日本だけで起こっているわけではないらしい。
http://www.google.mk/support/forum/p/Google+Mobile/thread?tid=1ea61a8c2fffdb2e&hl=en

システムモニタのアプリで調べてみたら、原因はシステムプロセスのsuspend。アプリではない。

下のスクリーンショットを見ると、端末がスリープに入るとsuspendプロセスがCPUを使い始めていることがわかる。

バッテリー履歴1日CPU消費量システムプロセスのCPU使用量suspendプロセスのグラフ

いまのところ、バッテリー消費が激しくなったら再起動するしかないようだ。

2011年6月6日月曜日

ASPIRE 1830Z-F52Cの消費電力

ワットチェッカーで測ってみた。ワットチェッカーは1W単位でしか測れないので測定値はかなり大雑把な値。仕事に使っていても大体4~5時間くらいは使えるので夏の電力不足の時期には重宝しそう。

アイドル時

ディスプレイ輝度最大 10W
ディスプレイ輝度最小 7W
ディスプレイ輝度中間(10段階のうち下から5番目) 8W

バイオハザード5ベンチ

最大26W (ディスプレイ輝度中間)

スリープ時

0W (小さすぎて測れませんでした。1W未満ということ。)

2011年6月1日水曜日

Slim3でDatastoreを使う

 

モデルの定義

データのクラスに@Modelアノテーションを付ける。
永続化するプロパティには getter, setter が必要。
永続化しないフィールドには@Attribute(persistent = false) アノテーションを付ける。

フィールドの型

フィールドで使える型はDatastoreの基本型、基本型のコレクション、シリアライズ可能なオブジェクト。
フィールドで使える基本型 → Core Value Types
シリアライズ可能なオブジェクトはBlob型として格納される(@Attribute(lob = true)を付ける)。
500文字(byte[]は500バイト)を超えるフィールドには@Attribute(lob = true)を付ける。
配列はコレクションとして扱われない(シリアライズしてBlobになる?)。
インデックスに対応した型はデフォルトでインデックスが付く。
インデックスを付けたくないフィールドには@Attribute(unindexed = true)を付ける。

サポートするコレクション型

ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet, List (ArrayList), Set (HashSet), SortedSet (TreeSet)

()内はプロパティから返される実際のインスタンスの型。

自動的なプロパティ値の更新

@Attribute(listener = ???) アノテーションで値を自動更新するリスナを指定できる。

保存するたびに更新するときは、

@Attribute(listener = ModificationDate.class)
Date updatedAt;

最初の保存のときだけ更新するときは、

@Attribute(listener = CreationDate.class)
Date createdAt;

キー

モデルクラスに1つだけ主キープロパティを持たなければならない。
キーのフィールドには @Attribute(primaryKey = true) を付ける。
キーは パス、kind、ID によって構成される。ネームスペースを使っているときはネームスペース名もキーに保存される。詳しくは「MultitenancyとNamespaces API」を参照。

kind はクラスの単純名から付けられる。kindはクラスの @Model(kind = "...") アノテーションで変更可能。

IDはアプリケーションで指定した文字列か、Datastoreが付けた数値(キーをnullのまま保存)。
自分で作ったIDからキーを作るとはき Datastore.createKey(…)。IDの自動割り当てでキーを作るときは Datastore.allocateId(…)。キーを作成したらモデルオブジェクトの主キープロパティにセットする。主キーをセットせずに Datastore.put するとキーは自動生成される。

リレーションシップ

片方向1対1関連

ModelRef型のプロパティを作って次のように初期化とgetterメソッドを作成。

@Model public class Address { … }

@Model public class Employee {
    private ModelRef<Address> addressRef =
      new ModelRef<Address>(Address.class);
    public ModelRef<Address> getAddressRef() { return addressRef; }
}

インスタンスを作成して関連付けして保存

Address address = new Address();
Employee employee = new Employee();
employee.getAddressRef().setModel(address);
Datastore.put(address, employee);

ロードするときは、

Employee employee = Datastore.get(Employee.class, employeeKey);
Address address = employee.getAddressRef().getModel();

参照先はレイジーロードされる。

双方向1対1関連

先ほどの関連に逆方向の参照を追加。逆方向はInverseModelRef型にする。逆方向の参照は永続化しない。Addressクラスに以下のコードを追加。

@Attribute(persistent = false)
private InverseModelRef<Employee, Address> employeeRef =
  new InverseModelRef<Employee, Address>
  (Employee.class, "addressRef", this);

public InverseModelRef<Employee, Address> getEmployeeRef() {
    return employeeRef;
}

これでAddressからEmployeeへの参照ができる。

address.getEmployeeRef.getModel();

プロパティに関連をセットするには正方向側(Employee側)のプロパティにセットする。逆方向にはsetModel()メソッドが無い。

片方向多対1関連

多 → 1 の参照を持つ関連の場合、片方向1対1の場合と同じ。

双方向多対1関連

多 → 1への参照に加え、1 → 多 への参照を追加。1側から多側への参照は永続化しない。

@Attribute(persistent = false)
private InverseModelListRef<Employee, Department> employeeListRef =
  new InverseModelListRef<Employee, Department>
  (Employee.class, "departmentRef", this);

public InverseModelListRef<Employee, Department> getEmployeeListRef()
{…}

1側から多側を取得する場合は次のようにする。

List<Employee> employeeList = department.getEmployeeListRef()
  .getModelList();

双方向多対多関連

関連クラスを作って、1 ← 多(関連クラス) → 1 のようにする。

関連クラスに1側への参照プロパティをそれぞれ作る。

@Model
public class EmployeeProject {
    …
    private ModelRef<Employee> employeeRef =
      new ModelRef<Employee>(Employee.class);
    private ModelRef<Project> projectRef =
      new ModelRef<Project>(Project.class);

    public ModelRef<Employee> getEmployeeRef() {…}
    public ModelRef<Project> getProjectRef() {…}
    …
}

1側は"双方向多対1"の逆参照と同様。EmployeeProjectに対して永続化しない InverseModelListRef<EmployeeProject, 1側クラス> のプロパティとgetterを作る。

ロードするときは片側(たとえばProjectオブジェクト)から関連オブジェクトのリストを取得して、関連オブジェクトから反対側のオブジェクトを得る。

relObject = project.getEmployeeProjectRef().getModelList();
relObject.getEmployeeRef().getModel();

キーの作成

アプリ指定の値で作成

Datastore.createKey(…);

値を自動割り当て

Datastore.allocateId(…);

親エンティティを指定して作成

Datastore.createKey(parentKey, Child.class, value);
Datastore.allocateId(parentKey, Clind.class);

データオブジェクトの新規追加

org.slim3.datastore.DataStore.put(object);

非同期での保存も可能。

DataStore.putAsync(object);

複数のオブジェクトの保存はコレクションか配列をputに渡すか、putに複数のパラメータを並べる

オブジェクトのロード

Datastore.get(Class, Key);

複数のオブジェクトの取り出しもできる。

DataStore.get(Class, コレクション or 配列);
Datastore.get(Class, key1, key2, …);

オブジェクトの更新

オブジェクトを更新するときは、ロード(get)、オブジェクトの値の変更、保存(put)の順で行う。

オブジェクトの削除

Datastore.delete(Key);

ロードや更新と同様、複数オブジェクトを同時に削除できる。
子孫エンティティごと削除するには

Datastore.deleteAll(Key);

クエリ

メタデータ

データクラスをEclipseで作成すると、自動的にメタデータが作成される。クエリの作成の際にこのメタデータを使う。

DataClassのメタデータのインスタンスは次のように取得できる。

DataClassMeta meta = DataClassMeta.get();

フィルタ

EntityQuery query = Datastore.query(meta);
List results = query.filter(
  meta.property1.equal(value1),
  meta.progerty2.greaterThan(value2), …
  ).asList();

インメモリでフィルタ。

endsWith や contains が使える。

query.filterInMemory(meta.property.endsWith("XYZ"))

ソート

query.sort(meta.property1.asc, meta.property2.desc)

インメモリでソート

query.sortInMemory(meta.property.asc)

offset, limit

query.offset(5).limit(10)

クエリの実行と結果

query.asList()
query.asIterator()
query.asSingle()
query.asKeyList()
query.asKeyIterator()

祖先クエリ

[TODO] 後で調べる

List<Child> list = Datastore.query(Child.class, ancestorKey)
  .asList();

kind なしの祖先クエリ

[TODO] 後で調べる

List<Entity> list = Datastore.query(ancestorKey).asList();

クエリの制限

不等式フィルタはクエリ中で1つのプロパティだけにしか使用できない。
不等式で使われたプロパティは最初のソート項目に無ければならない(ソートを指定しなければならない)。

インデックス

フィルタで指定したプロパティを持っていないエンティティは、クエリに引っかからない。
インデックスされないプロパティはクエリに引っかからない。
Text、Blob、@Attribute(unindexed = true) アノテーションをつけたプロパティはインデックスされない。
同じプロパティ名で別の型の値を持つエンティティがあると、型でソートされたあと値でソートされる。
対応するインデックス定義が無いクエリは失敗する。

インデックスの定義

単純なクエリに対するインデックスは自動作成される。

  • フィルタとソート順を使用しないクエリ
  • 等式フィルタと祖先フィルタのみを使用するクエリ
  • 単一のプロパティに対する不等式フィルタのみを使用するクエリ
  • フィルタなしで、プロパティに昇順か降順のどちらかのソート順が設定されているクエリ
  • 等式フィルタをプロパティに使用して、不等式または範囲フィルタをキーに使用しているクエリ

インデックスはxmlで定義。
クエリに必要なインデックス定義は開発環境でクエリを実行したときに自動的に作成される。

手作業で定義するときは WEB-INF/datastore-indexes.xml に書く。

datastore-indexes.xml の autoGenerate が true になっている場合、WEB-INF/appengine-generated/datastore-indexes-auto.xml にインデックスが自動生成される。

クエリカーソル

カーソルとは、フェッチ処理後の次の位置を表す文字列。
S3QueryResultList を通じてカーソルを使用する。

S3QueryResultList results = query.limit(20).asQueryResultList();
results.getEncodedCursor();
results.getEncodedFilters();
results.getEncodedSorts();
results.hasNext();

query.encodedCursor(encodedCursor)
  .encodedFilters(encodedFilters)
  .encodedSorts(encodedSorts)
  .limit(20).asQueryResultList();

カーソルの制限
  • in や != フィルタのクエリには使えない
  • カーソルは同じクエリにしか使えない。kind、フィルタ、フィルタ値、祖先フィルタ、ソートが同じであること。
  • インデックス設定を変更するとクエリは無効になる

 

エンティティグループ

エンティティの親子関係はキーを作成するときに親エンティティのIDを指定することで決定する
プロパティに他のエンティティの参照を持ってもエンティティグループに入れるわけではない
エンティティの親子関係はあとで変えることはできない
先祖エンティティが削除されても子孫エンティティは削除されない

トランザクション

Transaction tx = Datastore.beginTransaction();
DataClass data = Datastore.get(tx, Dataclass.class, key);
data.setProperty(value);
Datastore.put(tx, data);
tx.commit();

単一のトランザクションでできることは

  • 1つのグループ内の複数のエンティティを変更すること
  • グループに新しいエンティティを追加すること

1つのトランザクション内では1つのエンティティグループの操作だけができる
(エンティティのロードも含めて)。
楽観的並列処理なのでトランザクションが失敗したらアプリ側で何度かリトライする。
トランザクションの外側のトランザクション分離性はRead committedに近いがトランザクションはSerializableで実行される。
トランザクション内の祖先クエリやgetはトランザクション内での変更があってもトランザクション開始時点のスナップショットを返す。

バージョンによる楽観的ロック

トランザクションを超える更新(Web画面のためのGETリクエストの後、更新のためのPOSTリクエストでDatastoreにputするなど)を行う場合、データのバージョンにより競合を検出できる。

エンティティに @Attribute(version = true) を付けたプロパティを用意することで、書き込みためのgetの際にすでに他の書き込みによってバージョンが変更されていたら ConcurrentModificationException が投げられる。

Datastore.get(Transaction, Class, Key, version)

クロスグループ(XG)トランザクション (2013/10/12追加)

まず、Datastore が HRD で動作するよう設定する。
次にappengine-web.xml の system-properties要素に

<property name="slim3.useXGTX" value="true"/>

を加える。これがないとローカル環境でXGが使えない。
あとは通常通りトランザクションを使用するだけで1つのトランザクションで複数のグループを扱うことができる。

グローバルトランザクション (2013/10/12削除)

複数のエンティティグループにまたがったトランザクションを実行できる。

GlobalTransaction gtx = Datastore.beginGlobalTransaction();
gtx.get(…);
gtx.put(…);
gtx.commit();

同じエンティティグループに対してローカルトランザクションとグローバルトランザクションを混ぜて使ってはいけない。グローバルトランザクションはパフォーマンス的にはローカルトランザクションと大きな違いはないそうなので、グローバルトランザクションを使う可能性があればそのエンティティグループの操作には常にグローバルトランザクションを使うと良い。

2011年5月31日火曜日

MultitenancyとNamespaces API (Google App Engine)

 

Multitenancy

Multitenancyとは1つのアプリケーションインスタンスが複数のクライアント(ユーザーグループ)に対してサービスを行うソフトウェアアーキテクチャ。

ドキュメントによは、次のような用途が記載されている。

  • ユーザーインフォメーションを分類する
  • アドミニストレーターデータをアプリケーションデータから分離する
  • テストと完成品のために、分離されたデータストアインスタンスを作り出す
  • 多数のアプリケーションを一つのAppEngineのインスタンス上で走らせる

つまりうっかり混ざってほしくないデータを分けて保存したい場合に使えるようだ。Namespace APIを使ってネームスペース設定することでデータをネームスペースごとに分けて保存することができる。

ネームスペースに対応したAppEngine API

ネームスペースが使えるのは

  • Datastore
  • Memcache
  • Task Queue

だけ。

Blobstoreではネームスペースは使えない。

Namespace APIを使う

Multitenancyは Namespaces APIを使って実現する。使い方は簡単で、NamespaceManager.set(String) メソッドを呼ぶだけでカレントネームスペースを設定できる。

ネームスペースに設定できる文字列は100文字までの英数字、'-'、'_'、'.'で構成された文字列。アンダーバーで始まる文字列はシステム予約なので使えない。

ネームスペースを設定しなければカレントネームスペースはnullで、この場合各種AppEngine APIは空文字列("")のネームスペースを使用する。

ネームスペースはリクエストごとに設定する必要がある。各リクエストが開始された時点ではカレントネームスペースは設定されていない。

データ漏洩を避ける

ネームスペースで分離したデータがネームスペースを超えて漏洩しないように注意しなければならない。ネームスペースは文字列ひとつで設定できるのでうっかりすればネームスペースを超えたデータにアクセスできてしまう。

Blobsoreはネームスペースをサポートしないので、ネームスペースを使うアプリケーションでは、blobキーをネームスペースつきのDatastoreに保存してこのDatastoreデータを経由してblobにアクセスする。ブラウザから渡されたblobキーで直接blobにアクセスしてしまうと分離したいデータにアクセスできてしまうかもしれない。

Datastore

Datastore APIを呼び出す前にNamespaceManagerでカレントネームスペースを設定しておくだけで良い。

KeyやQueryを作るとき、APIはカレントネームスペースを参照してKey、Queryオブジェクトにネームスペースを設定する。先祖を指定してキーを作成すると、新しいキーは先祖キーのネームスペースを継承する。
なお、KeyやQueryに明示的にネームスペースを設定するJava APIは無い。

Key、Queryオブジェクトをシリアライズしたデータにはネームスペースも含まれる。これらをデシリアライズして使うときにはネームスペースが適切かどうか注意する必要がある。

カレントネームスペースが”a”のときに作られたキーは、シリアライズされてカレントネームスペース”b”でデシリアライズされてもキーのネームスペースは”a”のままで復元される。これをDatastoreにストアするとネームスペース”a”にストアされる。

信用できないソース(ブラウザなど)から渡されたKeyでDatastoreにアクセスするとネームスペースを超えてデータにアクセスしてしまう危険があるので、受け取ったキーのネームスペースが適切かどうか検証してから使用すること。

Memcache

MemcacheService を作成するときに明示的にネームスペースを設定しなければ、MemcacheService はメソッドが呼ばれるときにカレントネームスペースの設定を参照して memcache にアクセスする。

下のコード例はドキュメントから引用。

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current =
  MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
    current.put("key", value);  // stores value in namespace “abc”
} finally {
    NamespaceManager.set(oldNamespace);
}

MemcacheServiceFactory.getMemcacheService(String) で明示的にネームスペースを指定すると、カレントネームスペースを無視して指定したネームスペースで memcache にアクセスする。

MemcacheService boundMemcache =
  MemcacheServiceFactory.getMemcacheService("specific-namespace");
NamespaceManager.set("whatever-namespace");
// このレコードはネームスペース"specific-namespace"でストアされる。
boundMemcache.put("key3", "value3");

Task Queue

タスクが作られるときにNamespaceManagerにセットしたカレントネームスペースとGoogle Appsドメインも(もしあれば)共にキューにプッシュされる。

タスクが実行されるとき、カレントネームスペースとGoogle Appsドメインが復元される。もしタスクをaddするときにカレントネームスペースが設定されていなければ、タスクが実行されるとき空のネームスペースが設定される。

タスク名はネームスペースで分離されないのですべてのネームスペースに渡ってユニークな名前にしないとぶつかる。

Queueからタスクをプルする場合はネームスペースの機能は提供されない。自前でタスクのネームスペースを復元できるようにペイロードにネームスペースを入れるなどの策が必要。

API

Class NamespaceManager

カレントネームスペースを操作する機能を提供する。

カレントネームスペースはget()によって返される文字列。Datastore、Memcache、Task QueueのAPIで使われる。

ネームスペースに関連したクラス(例えば、Key、Query、MemcacheService)が作られるとき、ネームスペースが決まっていなければget()の呼び出しで決定される。もしget()がnullを返したらカレントネームスペースはセットされておらずこれらのAPIは空の("")ネームスペースを使う。

例:

NamespaceManager.set("a-namespace");
MemcacheService memcache =
  MemcacheServiceFactory.getMemcacheService();
// Store record in namespace "a-namespace"
memcache.put("key1", "value1");

NamespaceManager.set("other-namespace");
// Store record in namespace "other-namespace"
memcache.put("key2", "value2");

MemcacheService boundMemcache =
    MemcacheServiceFactory.getMemcacheService("specific-namespace");
NamespaceManager.set("whatever-namespace");
// このレコードはネームスペース"specific-namespace"でストアされる。
boundMemcache.put("key3", "value3");

MemcacheService memcache (上記の例で)はカレントネームスペースを使う。そして key1 がネームスペース「a-namespace」にストアされる。key2 はネームスペース「other-namespace」にストアされる。カレントネームスペースに優先してデータを特定のネームスペースにストアもできる。上記の例で key3 はネームスペース「specific-namespace」にストアされる。

Task Queueの Queue.add() メソッドは追加されるタスクの中にNamaspaceManagerの設定を転送し、追加されたタスクはタスクを作ったものと同じカレントネームスペースで実行される。

例外的に、カレントネームスペースが設定されていないとき(つまりget()がnullを返すとき)は空のネームスペース("")が作られたタスクのリクエストに転送される。

メソッド

public static void set(java.lang.String newNamespace)

    ネームスペースに関連したサービスのネームスペース初期化に使われる値をセットする。

    Parameters:
        newNamespace - 新しいネームスペース。
    Throws:
        java.lang.IllegalArgumentException - ネームスペース文字列が正しくないとき。

public static java.lang.String get()

    カレントネームスペース設定またはnull(設定されていないとき)返す。

    もしカレントネームスペースが設定されていなければ、呼び出し元は ネームスペースに関連したすべてのサービスで空のネームスペース(“”)を使うべきである。

public static java.lang.String getGoogleAppsNamespace()

    このリクエストのGoogle Appsドメインを返す。または、空の文字列を返す。

public static void validateNamespace(java.lang.String namespace)

    ネームスペース文字列の妥当性を検証する。

    Throws:
        java.lang.IllegalArgumentException - ネームスペース文字列のフォーマットが正しくないとき。

2011年5月28日土曜日

アプリケーションへのアクセスを制限する (Google App Engine)

URLパターンでアクセスを制限する

特定のURLパターンに対してアクセスを制限するには web.xml のsecurity-constraint要素で定義できる。

web-app要素(ルートの要素)の下にsecurity-constraint要素を追加する。

<security-constraint>

    <web-resource-collection>
        <web-resource-name>admin tool</web-resource-name>
        <url-pattern>/admin-tool*</url-pattern>
    </web-resource-collection>

    <user-data-constraint>
        <role-name>admin</role-name>
    </user-data-constraint>

</security-constraint>

アクセス可能なロールをadminとしておくと、AppEngineアプリの設定画面で管理者として設定したGoogleアカウントでしかアクセスできなくなる。Googleにログインせずに該当のURLにアクセスした場合は自動的にGoogleアカウントへのログイン画面へリダイレクトされる。

プログラムで動的にアクセスを制限する

開発環境とGAEのサーバー環境でアクセス制限方法を変えたり、非公開バージョンだけにアクセス制限をかけたい場合など、security constraint では柔軟性に欠ける場合にはアプリ自前のコードでアクセス制限をかけることができる。ただし、静的ファイルにはこの方法は使えない。

ここで言う”管理者”もsecurity-constraintの場合と同じ、アプリの設定画面で設定したGoogleアカウントのことを指す。

次のAPIはアクセス制限の役に立つ。

SystemProperty.applicationId
リクエストされたホスト名がアプリのホスト名かどうか判断するときに使う。

SystemProperty.environment
アプリが稼動する環境が開発環境かGAEサーバー環境かを判断するときに使う。

UserService#isUserAdmin()
Googleアカウントにログイン中のユーザーがアプリの管理者かどうかを判断するときに使う。

HttpServletRequest#getUserPrincipal()
ユーザーがログインしているかを判断するときに使う。

コード例

UserService userService = UserserviceFactory.getUserService();
if ( request.getUserPrincipal() != null
  && userService.isUserAdmin() ) {
    // アクセス許可
}
else {
    // アクセス拒否
}

2011年4月25日月曜日

Apache Antを使ってビルドする (Google App Engine)

 

App EngineのアプリのビルドはEclipseプラグインを使うと簡単だが、Antを使うこともできる。

以下、Google App Engineのドキュメントに沿って説明する。コード例などはドキュメントから引用。

プロジェクトのディレクトリ構成

App Engineアプリの典型的なディレクトリ構成は次のとおり。これ以降このディレクトリ構成を前提にする。

プロジェクトルート/
  src/
    パッケージ/Javaソースコード
    META-INF/
      設定ファイルなど
  war/
    Web用のファイル (JSP, 画像などの静的ファイル)
    WEB-INF/
      アプリの設定ファイルなど
      classes/
        classファイル
      lib/
        ライブラリのJAR

ビルドファイル

build.xmlには以下の設定をする。

App EngineのSDKの場所。自分の環境に合わせて設定する。

<property name="sdk.dir" location="../appengine-java-sdk" />

Antマクロのインポート。

<import file="${sdk.dir}/config/user/ant-macros.xml" />

コンパイルのためのクラスパスの定義

コンパイルに必要なJARをクラスパスに追加する。以下の場所をクラスパスに追加する。

  • プロジェクトに自分で追加したjarファイル
  • App Engine SDKのJARファイル

<path id="project.classpath">
  <pathelement path="war/WEB-INF/classes" />
  <fileset dir="war/WEB-INF/lib">
    <include name="**/*.jar" />
  </fileset>
  <fileset dir="${sdk.dir}/lib">
    <include name="shared/**/*.jar" />
  </fileset>
</path>

JARのコピー

App Engine SDKのJARをwar/WEB-INF/libにコピーする。

<target name="copyjars"
    description="Copies the App Engine JARs to the WAR.">
  <copy
      todir="war/WEB-INF/lib"
      flatten="true">
    <fileset dir="${sdk.dir}/lib/user">
      <include name="**/*.jar" />
    </fileset>
  </copy>
</target>

ソースファイルのコンパイル

普通にコンパイルするだけ。

まず、コンパイルしたclassファイルを入れるディレクトリを作成。次に出力ディレクトリにリソースをコピー(Javaのソースファイルはコピーから除外)。そしてコンパイル。

<target name="compile" depends="copyjars"
  description="Compiles Java source and copies other source files to the WAR.">

  <mkdir dir="war/WEB-INF/classes" />
  <copy todir="war/WEB-INF/classes">
    <fileset dir="src">
      <exclude name="**/*.java" />
    </fileset>
  </copy>
  <javac
      srcdir="src"
      destdir="war/WEB-INF/classes"
      classpathref="project.classpath"
      debug="on" />
</target>

JDOファイルの拡張

JDOを使わないならこのプロセスは不要。

JDOを使うためにはJDOアノテーションをつけたクラスに対してコンパイル後のclassファイルに後処理が必要。

App EngineのAntマクロ<enhance_war>を使う。

<target name="datanucleusenhance" depends="compile"
    description="Performs JDO enhancement on compiled data classes.">
  <enhance_war war="war" />
</target>

開発用サーバの起動

開発用サーバもAntマクロで起動できる。

<target name="runserver" depends="datanucleusenhance"
  description="Starts the development server.">
  <dev_appserver war="war" />
</target>

より詳細な設定もできる。サーバの起動ポート、JavaVMの引数を指定する方法はドキュメントによると以下のとおりだが、このデバッグオプションを指定するとなぜか正常にJettyが起動しなかった。

<dev_appserver war="war" port="8888" >
   <options>
     <arg value="--jvm_flag=-Xdebug"/>
     <arg value="--jvm_flag=-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9999"/>
   </options>
</dev_appserver>

サーバを停止するには Ctrl + C を押すとドキュメントにはあるが、これではAndのプロセスが止まるだけでサーバのプロセスは止まっていないようなので、Windowsの場合ならタスクマネージャーでプロセスを終了させる必要がある。

App Engine へのアップロード

<appcfg>マクロを使う。これはApp Engine SDKのツールAppCfgを呼び出すためのマクロ。パラメータなど詳細はAppCfgのドキュメントを参照。

<target name="update" depends="datanucleusenhance"
    description="Uploads the application to App Engine.">
  <appcfg action="update" war="war" />
</target>

AppCfgタスク

アプリのアップデート以外のAppCfgは次のとおり。

datastoreのインデックスを更新する。アプリをアップデートする前にインデックスだけ更新するときに使う。

<target name="update_indexes" depends="datanucleusenhance"
  description="Uploads just the datastore index configuration to App Engine.">
  <appcfg action="update_indexes" war="war" />
</target>

ロールバック。アプリの更新が部分的に完了した場合に元に戻す。

<target name="rollback" depends="datanucleusenhance"
  description="Rolls back an interrupted application update.">
  <appcfg action="rollback" war="war" />
</target>

ログのダウンロード。

<target name="request_logs"
  description="Downloads log data from App Engine for the application.">
  <appcfg action="request_logs" war="war">
    <options>
      <arg value="--num_days=5"/>
    </options>
    <args>
      <arg value="logs.txt"/>
    </args>
  </appcfg>
</target>

ターゲットの依存関係

ここまでの説明で登場したターゲットの実行順は次のようになっている。

copyjars → compile → datanucleusenhance → runserver

[省略] → datanucleusenhance → update

[省略] → datanucleusenhance → update_indexes

[省略] → datanucleusenhance → rollback

request_logs

2011年3月2日水曜日

Blobstoreドキュメント日本語要約 (Google App Engine)

 

概要

BlobstoreはDatastoreより大きなblobオブジェクトを扱うことができる。BlobはHTTPリクエストでファイルをアップロードすることで作られる。ファイルがアップロードされるとBlobstoreがblob keyを返す。blogを操作するためにこのキーを使う。

blobのサイズは最大で2GB。
Blobstoreのblobは、Datastoreのblobプロパティとは別のもの。
blobは削除することはできるが変更することはできない。

blobの作成時刻やコンテントタイプなどの情報はDatastoreに記録される。blobキーはblobの情報を読み出すクエリに使うことができる。

Blobstoreの使い方

アプリはblobに直接アクセスできない。かわりにDatastoreのblob情報エンティティを通してblobを扱える。

ユーザはHTMLフォームの1つまたは複数のファイル入力フィールドからblobを作ることができる。

アプリはblobstoreService.createUploadUrl()で得られるURLをフォームのactionにセットする。このメソッドにはアプリのハンドラのパスを渡す。ユーザはBlobstoreに直接ファイルをアップロードする。

Blobstoreはユーザのリクエストを書き換えてアップロードされたファイルをストアし、blobキーに対応するファイルデータを置き換える。そして書き換えられたリクエストをあなたがcreateUploadUrl()に提供したURLパスのハンドラに渡す。

ハンドラはblobキーに基づいた追加の処理ができる。

最後に、ハンドラはヘッダのみのリダイレクトレスポンス(301, 302, 303)を返して、典型的にはブラウザは、blobアップロードのステータスを示す別のページにリダイレクトする。

アプリはファイルのようなストリーミングインタフェースを使ってBlobstoreのデータを読むことができる(BlobstoreInputStream)。

blobをアップロードする

フォームの例。

<form action="<%= blobstoreService.createUploadUrl("/upload") %>"
  method="post" enctype="multipart/form-data">
    <input type="file" name="myFile">
    <input type="submit" value="Submit">
</form>

actionのURLはcreateUploadUrl()で作成。メソッドにはアップロード完了で呼ばれるハンドラのURLを渡す。

フォームにはファイルinputのフィールドを含む必要がある。enctypeは"multipart/form-data"にする。

ハンドラが呼ばれるときにはすでにblobは保存され、Datastoreにblob情報ができている。

ハンドラでアップロードされたblobのリストを取り出す例。Mapには<ファイル名,BlobKey>の組で入っている。ファイル名はアップロードの際にフォームに書いた値。

Map<String, BlobKey> blobs = blobstoreService.getUploadedBlobs(req);

blobstoreがユーザリクエストを書き換えるとき、アップロードされたファイルのMIMEパートはbodyが空にして、MIMEパートのヘッダにblobキーを追加する。他のすべてのフォームフィールドとパートはそのままハンドラに渡される。

コンテントタイプを指定しない場合、Blobstoreはファイルの拡張子からコンテントタイプを推定する。コンテントタイプが決定されないときは、新しく作られたblobは application/octet-streamが割り当てられる。

blobを供給する

blobを供給するためにはアプリでblobのダウンロードハンドラを用意する必要がある。ハンドラでは欲しいblobのキーをblobstoreService.serve(blobKey, res)メソッドに渡す。

例:

BlobKey blobKey = new BlobKey(req.getParameter("blob-key"));
blobstoreService.serve(blobKey, res);

blobはアプリのどんなURLからも供給できる。アプリでblobを供給するために、blobキーを含んだ特別なヘッダをレスポンスに含める。App Engineはレスポンスのボディをblobの内容で置き換える。

blobのバイト範囲

レスポンスにX-AppEngine-BlobRangeヘッダを含めることで大きなデータの一部だけを標準的なHTTPバイトレンジで返すことができる。

X-AppEngine-BlobRangeをブランクにするとレンジヘッダを無視して完全なblobを返す。

範囲指定の例:

  • 0-499 最初の500バイト
  • 500-999 501バイト目から500バイト
  • 500- 501バイト目から最後まで
  • -500 最後の500バイト

バイト範囲が正しければBlobstoreはクライアントに"206 Partial Content"のステータスコードと要求されたバイト範囲を送る。もしレンジが正しくなければBlobstoreは"416 Requested Range Not Satisfiable"を送る。

Blobstoreは1つのリクエストで複数のバイト範囲をサポートしない。

Blobstoreでイメージサービスを使う

イメージサービスはBlobstoreのデータを変換のソースとして使用できる。ソースイメージはBlobstoreデータの最大サイズまで扱える。変換されたイメージは1MBより小さくなければならない。

詳しくはイメージサービスのドキュメントを参照。

ファイルをBlobstoreに書き込む (実験的)

プログラムからblobデータを読み書きするためのAPIが用意されている。この機能によってデータをエクスポートしたり、生成したバイナリデータを保存したりできる。

つきのようにして書き込み可能な空のファイルを作成し、書き込み可能な Channel をオープンする。

AppEngineFile file =
  fileService.createNewBlobFile("text/plain");

FileWriteChannel writeChannel =
  fileService.openWriteChannel(file, lock);

たとえ1バイトを書くだけでも、それぞれの書き込みが最小のCPUクォータを消費する。書き込みをバッチ処理することによって、効率的にクォータを使うことができる。
(訳注:この「書き込み」の単位がメソッド呼び出しなのかファイルをクローズするまでなのかは不明。)

クォータとリミット

Blobstoreデータに使われたスペースは保存データのクォータ(課金対象)に計上される。
Datastoreに含まれるblob情報エンティティがDatastore関連のクォータに計上される。
Blobstoreの操作によって消費されたCPU時間はCPU時間のクォータ(課金対象)に計上される。

詳しくはクォータの情報はクォータと管理コンソールの「クォータ詳細情報」のドキュメントを参照。

リミット

  • 最大オブジェクトサイズ 2GB
  • 1回のAPI呼び出しで読めるBlobstoreデータの最大のサイズ 1MB

2011年2月27日日曜日

Channel API ドキュメント日本語要約 (Google App Engine)

 

概要

Channel APIはアプリケーションとgoogleサーバーの間に持続的な接続を作り、JavaScriptクライアントにポーリングを使わずにリアルタイムにメッセージを送ることができる。

Channel APIの要素

JavaScriptクライアント

クライアントの役割

  • チャネルに接続してサーバからチャネルのユニークなトークンを受け取る。
  • ほかのクライアントのアップデートを待ち受けて、データを使用する。
  • アップデートをサーバーに送信する。
サーバ

サーバの役割

  • 個々のJavaScriptクライアントにユニークなチャネルを作る。
  • ユニークトークンを作りJavaScriptクライアントに送る。
  • クライアントからのアップデートをPOSTで受ける。
  • チャネルを使ってクライアントにアップデートメッセージを送る。
クライアントID

クライアントIDはサーバでそれぞれのJavaScriptクライアントを識別するためのもの。クライアントIDはアプリケーションでどのように設計しても良い。

トークン

トークンはJavaScriptクライアントがチャネルに接続して待ち受けることを可能にする役割がある。

サーバはそれぞれのクライアントのためにクライアントIDと有効期限などの情報を使って トークンを作成する。

チャネル

チャネルはサーバーがクライアントIDで識別されるJavaScriptクライアントにアップデートを送るための片方向の通信路。

サーバはクライアントからPOSTによってアップデートを受信すると、該当するクライアントにチャネルを使ってメッセージを送る。

メッセージ

メッセージはクライアントからサーバにPOSTによって送られる。

受信すると、サーバはクライアントIDで識別されたクライアントにチャネルを使ってメッセージを渡す。

メッセージは32KBまで。

URLを送るときに生のメッセージで送るのは避ける。その代わりにメッセージが損なわれずに到達するようにJSONエンコーディングを使う。

ソケット

JavaScriptクライアントはサーバから提供されたトークンを使ってソケット開く。チャネルで更新を待ち受けるためにソケットを使う。

Channel APIを使うためのコード

チャネルを作る(サーバ)

channelServiceを取得して、チャネルを作成。トークンを生成する。

ChannelService channelService =
  ChannelServiceFactory.getChannelService();
String token = channelService.createChannel(clientId);

チャネルに接続する(クライアント)

サーバからもらったトークンを使ってチャネルを開く。

channel = new goog.appengine.Channel(token);
socket = channel.open();
socket.onopen = onOpened;
socket.onmessage = onMessage;
socket.onerror = onError;
socket.onclose = onClose;

サーバーからクライアントへのメッセージ送信(サーバ)

channelService.sendMessage(
  new ChannelMessage(clientId, messageString));

トークンとセキュリティ

createChannel()によって返されるトークンは他に知られないように秘密にしておく。もしトークンが盗まれたらチャネルに送られたメッセージを待ち受けできてしまう。

トークンは2時間で期限切れになる。クライアントが2時間以上チャネルに接続したままでいると、ソケットのonerror(), onclose()がコールバックされる。

注意

クライアントID毎に1つのクライアント

1つのクライアントIDで一度にチャネルに接続できるのは1つのクライアントだけ。アプリケーションは1つのクライアントIDでメッセージを多数のクライアントにばら撒くことはできない。

ページ毎に1つのチャネル、チャネル毎に1つのクライアント

クライアントは1ページにつき1つのチャネルにだけ接続できる。

もしアプリケーションがクライアントに複数の種類のデータを送る必要があるならサーバサイドでデータを集めてクライアントのソケットのonmessageハンドラに送る。

クライアントの接続と切断の追跡

チャネルに接続したクライアントには"プレゼンス"の概念がない。これはクライアントのチャネルへの接続または切断についてアプリケーションに通知されないことを意味する。

もし、アプリケーションがクライアントの接続を追跡する必要があるなら、1つの方法としてクライアントの状態を通知するために適当な間隔でPOSTメッセージをサーバに送るよう設計された関数をソケットのonopenプロパティにセットする。

2011年2月10日木曜日

Slim3でDatastoreを使う - データ操作編

 

この記事はこちらへ移動しました。
Slim3でDatastoreを使う

 

キーの作成

アプリ指定の値で作成
Datastore.createKey(…);

値を自動割り当て
Datastore.allocateId(…);

親エンティティを指定して作成
Datastore.createKey(parentKey, Child.class, value);
Datastore.allocateId(parentKey, Clind.class);

データオブジェクトの新規追加

org.slim3.datastore.DataStore.put(object);

非同期での保存も可能。
DataStore.putAsync(object);

複数のオブジェクトの保存はコレクションか配列をputに渡すか、putに複数のパラメータを並べる

オブジェクトのロード

Datastore.get(Class, Key);

複数のオブジェクトの取り出しもできる。
DataStore.get(Class, コレクション or 配列);
Datastore.get(Class, key1, key2, …);

オブジェクトの更新

オブジェクトを更新するときは、ロード(get)、オブジェクトの値の変更、保存(put)の順で行う。

オブジェクトの削除

Datastore.delete(Key);

ロードや更新と同様、複数オブジェクトを同時に削除できる。
子孫エンティティごと削除するには

Datastore.deleteAll(Key);

クエリ

メタデータ

データクラスをEclipseで作成すると、自動的にメタデータが作成される。クエリの作成の際にこのメタデータを使う。

DataClassのメタデータのインスタンスは次のように取得できる。

DataClassMeta meta = DataClassMeta.get();

フィルタ

EntityQuery query = Datastore.query(meta);
List results = query.filter( meta.property1.equal(value1), meta.progerty2.greaterThan(value2), … ).asList();

インメモリでフィルタ。
endsWith や contains が使える。
query.filterInMemory(meta.property.endsWith("XYZ"))

ソート

query.sort(meta.property1.asc, meta.property2.desc)

インメモリでソート
query.sortInMemory(meta.property.asc)

offset, limit

query.offset(5).limit(10)

クエリの実行と結果

query.asList()
query.asIterator()
query.asSingle()
query.asKeyList()
query.asKeyIterator()

祖先クエリ

[TODO] 後で調べる
List<Child> list = Datastore.query(Child.class, ancestorKey).asList();

kind なしの祖先クエリ

[TODO] 後で調べる
List<Entity> list = Datastore.query(ancestorKey).asList();

クエリの制限

不等式フィルタはクエリ中で1つのプロパティだけにしか使用できない。
不等式で使われたプロパティは最初のソート項目に無ければならない(ソートを指定しなければならない)。

インデックス

フィルタで指定したプロパティを持っていないエンティティは、クエリに引っかからない。
インデックスされないプロパティはクエリに引っかからない。
Text、Blob、@Attribute(unindexed = true) アノテーションをつけたプロパティはインデックスされない。
同じプロパティ名で別の型の値を持つエンティティがあると、型でソートされたあと値でソートされる。
対応するインデックス定義が無いクエリは失敗する。

インデックスの定義

単純なクエリに対するインデックスは自動作成される。

  • フィルタとソート順を使用しないクエリ
  • 等式フィルタと祖先フィルタのみを使用するクエリ
  • 単一のプロパティに対する不等式フィルタのみを使用するクエリ
  • フィルタなしで、プロパティに昇順か降順のどちらかのソート順が設定されているクエリ
  • 等式フィルタをプロパティに使用して、不等式または範囲フィルタをキーに使用しているクエリ

インデックスはxmlで定義。
クエリに必要なインデックス定義は開発環境でクエリを実行したときに自動的に作成される。

手作業で定義するときは WEB-INF/datastore-indexes.xml に書く。

datastore-indexes.xml の autoGenerate が true になっている場合、WEB-INF/appengine-generated/datastore-indexes-auto.xml にインデックスが自動生成される。

クエリカーソル

カーソルとは、フェッチ処理後の次の位置を表す文字列。
S3QueryResultList を通じてカーソルを使用する。

S3QueryResultList results = query.limit(20).asQueryResultList();
results.getEncodedCursor();
results.getEncodedFilters();
results.getEncodedSorts();
results.hasNext();

query.encodedCursor(encodedCursor)
.encodedFilters(encodedFilters)
.encodedSorts(encodedSorts)
.limit(20).asQueryResultList();

カーソルの制限
  • in や != フィルタのクエリには使えない
  • カーソルは同じクエリにしか使えない。kind、フィルタ、フィルタ値、祖先フィルタ、ソートが同じであること。
  • インデックス設定を変更するとクエリは無効になる

 

トランザクション

Transaction tx = Datastore.beginTransaction();
DataClass data = Datastore.get(tx, Dataclass.class, key);
data.setProperty(value);
Datastore.put(tx, data);
tx.commit();

単一のトランザクションでできることは

  • 1つのグループ内の複数のエンティティを変更すること
  • グループに新しいエンティティを追加すること

1つのトランザクション内では1つのエンティティグループの操作だけができる
(エンティティのロードも含めて)。
楽観的並列処理なのでトランザクションが失敗したらアプリ側で何度かリトライする。
トランザクションの外側のトランザクション分離性はRead committedに近いがトランザクションはSerializableで実行される。
トランザクション内の祖先クエリやgetはトランザクション内での変更があってもトランザクション開始時点のスナップショットを返す。

バージョンによる楽観的ロック

トランザクションを超える更新(Web画面のためのGETリクエストの後、更新のためのPOSTリクエストでDatastoreにputするなど)を行う場合、データのバージョンにより競合を検出できる。

エンティティに @Attribute(version = true) を付けたプロパティを用意することで、書き込みためのgetの際にすでに他の書き込みによってバージョンが変更されていたら ConcurrentModificationException が投げられる。

Datastore.get(Transaction, Class, Key, version)

グローバルトランザクション

複数のエンティティグループにまたがったトランザクションを実行できる。

GlobalTransaction gtx = Datastore.beginGlobalTransaction();
gtx.get(…);
gtx.put(…);
gtx.commit();

同じエンティティグループに対してローカルトランザクションとグローバルトランザクションを混ぜて使ってはいけない。グローバルトランザクションはパフォーマンス的にはローカルトランザクションと大きな違いはないそうなので、グローバルトランザクションを使う可能性があればそのエンティティグループの操作には常にグローバルトランザクションを使うと良い。

Slim3でDatastoreを使う - データ定義編

 

この記事はこちらへ移動しました。
Slim3でDatastoreを使う

 

モデルの定義

データのクラスに@Modelアノテーションを付ける
永続化するプロパティには getter, setter が必要
永続化しないフィールドには@Attribute(persistent = false) アノテーションを付ける

フィールドの型

フィールドで使える型はDatastoreの基本型、基本型のコレクション、シリアライズ可能なオブジェクト
フィールドで使える基本型 → Core Value Types
シリアライズ可能なオブジェクトはBlob型として格納される(@Attribute(lob = true)を付ける)
500文字(byte[]は500バイト)を超えるフィールドには@Attribute(lob = true)を付ける
配列はコレクションとして扱われない(シリアライズしてBlobになる?)
インデックスに対応した型はデフォルトでインデックスが付く
インデックスを付けたくないフィールドには@Attribute(unindexed = true)を付ける

サポートするコレクション型

ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet, List (ArrayList), Set (HashSet), SortedSet (TreeSet)

()内はプロパティから返される実際のインスタンスの型

自動的なプロパティ値の更新

@Attribute(listener = ???) アノテーションで値を自動更新するリスナを指定できる

保存するたびに更新

@Attribute(listener = ModificationDate.class)
Date updatedAt;

最初の保存のときだけ更新

@Attribute(listener = CreationDate.class)
Date createdAt;

キー

モデルクラスに1つだけキーを持たなければならない
キーのフィールドには @Attribute(primaryKey = true) を付ける
キーは パス、kind、ID によって構成される
kind はクラスの単純名から付けられる
kindはクラスの @Model(kind = "...") アノテーションで変更可能
IDはアプリケーションで指定した文字列か、Datastoreが付けた数値(キーをnullのまま保存)
キーは Datastore.createKey(Class, String) で作成してキープロパティにセットする

エンティティグループ

エンティティの親子関係はキーを作成するときに親エンティティのIDを指定することで決定する
プロパティに他のエンティティの参照を持ってもエンティティグループに入れるわけではない
エンティティの親子関係はあとで変えることはできない
先祖エンティティが削除されても子孫エンティティは削除されない

リレーションシップ

片方向1対1関連

ModelRef型のプロパティを作って次のように初期化とgetterメソッドを作成。

@Model public class Address { … }

@Model public class Employee {
    private ModelRef<Address> addressRef = new ModelRef<Address>(Address.class);
    public ModelRef<Address> getAddressRef() { return addressRef; }
}

インスタンスを作成して関連付けして保存

Address address = new Address();
Employee employee = new Employee();
employee.getAddressRef().setModel(address);
Datastore.put(address, employee);

ロードするときは、

Employee employee = Datastore.get(Employee.class, employeeKey);
Address address = employee.getAddressRef().getModel();

参照先はレイジーロードされる。

双方向1対1関連

先ほどの関連に逆方向の参照を追加。逆方向はInverseModelRef型にする。逆方向の参照は永続化しない。Addressクラスに以下のコードを追加。

@Attribute(persistent = false)
private InverseModelRef<Employee, Address> employeeRef = new InverseModelRef<Employee, Address>(Employee.class, "addressRef", this);

public InverseModelRef<Employee, Address> getEmployeeRef() { return employeeRef;}

これでAddressからEmployeeへの参照ができる。

address.getEmployeeRef.getModel();

プロパティに関連をセットするには正方向側(Employee側)のプロパティにセットする。逆方向にはsetModel()メソッドが無い。

片方向多対1関連

多 → 1 の参照を持つ関連の場合、片方向1対1の場合と同じ。

双方向多対1関連

多 → 1への参照に加え、1 → 多 への参照を追加。1側から多側への参照は永続化しない。

@Attribute(persistent = false)
private InverseModelListRef<Employee, Department> employeeListRef = new InverseModelListRef<Employee, Department>(Employee.class, "departmentRef", this);

public InverseModelListRef<Employee, Department> getEmployeeListRef() {…}

1側から多側を取得する場合は次のようにする。

List<Employee> employeeList = department.getEmployeeListRef().getModelList();

双方向多対多関連

関連クラスを作って、1 ← 多(関連クラス) → 1 のようにする。

関連クラスに1側への参照プロパティをそれぞれ作る。

@Model
public class EmployeeProject {
    …
    private ModelRef<Employee> employeeRef = new ModelRef<Employee>(Employee.class);
    private ModelRef<Project> projectRef = new ModelRef<Project>(Project.class);

    public ModelRef<Employee> getEmployeeRef() {…}
    public ModelRef<Project> getProjectRef() {…}
    …
}

1側は"双方向多対1"の逆参照と同様。EmployeeProjectに対して永続化しない InverseModelListRef<EmployeeProject, 1側クラス> のプロパティとgetterを作る。

ロードするときは片側(たとえばProjectオブジェクト)から関連オブジェクトのリストを取得して、関連オブジェクトから反対側のオブジェクトを得る。

relObject = project.getEmployeeProjectRef().getModelList();
relObject.getEmployeeRef().getModel();

2011年2月5日土曜日

GalaxySの解像度をAPIと実寸で比較 (Android)

Anddroid端末の解像度を取得する方法。
下のコードで画面のいろいろな情報を取得できる。

DisplayMetrics mtr = new DisplayMetrics();
context.getWindowManager().getDefaultDisplay().getMetrics(mtr);

GalaxySの画面の実寸サイズは長辺86mm 800px, 短辺52mm 480pxなので、長辺234.46153846153846153846153846154 dpi, 短辺 236.27906976744186046511627906977 dpi。

GalaxyS上でAPIで得られる数値はXdpi 234.46153, Ydpi 236.27907 なので実寸と同じ。こういうのはあまり信用できない数値が返ってくることが多いけど、試してみたGalaxySでは正確な数値が得られるようだ。

参考ページ

IS03、色々な機種のDpi情報が欲しいので、こちらからはGalaxy Sの情報を晒します

2011年1月16日日曜日

インテントを受け取れるアクティビティのリストを取得する (Android)

PackageManager の queryIntentActivities(Intent intent, int flags) メソッドで取得できる。

PackageManagerのインスタンスはActivityからgetPackageManager()メソッドで取得できる。

受け取って欲しいインテントを intentパラメータに指定する。flagsは

MATCH_DEFAULT_ONLY
GET_INTENT_FILTERS
GET_RESOLVED_FILTER

を指定できるが、フラグを設定しなくても(0を指定)動作するようだ。android.intent.category.DEFAULT のインテントを探すなら MATCH_DEFAULT_ONLY を指定する。

参考ページ

コードを貼り付けながら。「アプリケーションリストを取得する」

Y.A.M の 雑記帳「Android 特定の Intent (Action) を処理できる Activity (アプリ)の一覧を取得」

2011年1月12日水曜日

アクティビティとタスク (Android)

 

開発の基礎」の「アクティビティとタスク」の節がなかなか理解できなかったのでまとめ。

タスク

アクティビティのスタック。ブラウザでいえばHistoryみたいなもの。タスクそれぞれにアクティビティのスタックがある。

タスクはブラウザのタブみたいなもの。ただし、タブのように "見える化" されていなくてタスクに含まれるアプリケーションを再び起動しようとしたときにそのアプリケーションのタスクがフォアグラウンドになる。ただし、インテントのflagプロパティやアクティビティの起動モード定義によって挙動は変わる。

Affinity (親和性?)

アクティビティは自身と同じAffinityが設定されているタスクに所属しようとする。

タスクのAffinityはルートのアクティビティによって決まる。

アクティビティのAffinityはAndroidManifest.xmlのactivity要素のtaskAffinity属性で設定(たぶん任意の文字列で指定可能)。指定がなければ、application要素のtaskAffinity属性を継承する。アプリケーションのAffinityのデフォルトはmanifest要素のパッケージ名。

インテントにFLAG_ACTIVITY_NEW_TASK フラグが設定されている場合、新しいアクティビティは別のタスクに所属しようとするが、そのアクティビティと同じAffinityが設定されている既存のタスクがあればそこに追加される。なければ新しいタスクが開始される。

[あとで検証] もしFLAG_ACTIVITY_NEW_TASK フラグを設定したインテントで新しいアクティビティを起動するとき、startActivity()を呼び出したタスクと新しいアクティビティに同じAffinityが設定されていたら新しいタスクが起動するかどうか。たぶん新しいタスクはできないと予想。

activity要素に allowTaskReparenting="true" と設定されていると、このアクティビティと同じAffinityが設定されているタスクがフォアグラウンドに移ったときに、アクティビティを開始したタスクからそのタスクに移動できる。たとえば taskAffinity="A" のタスクで taskAffinity="B" のアクティビティを開始すると B は A のタスクに属するが、taskAffinity="B" のアプリケーションを起動すると B のアクティビティがこのタスクに移動する。

起動モード

activity要素の launchMode属性で定義。

"standard"

  • インテントを開始した(startActivity() を呼び出した)タスクに保持される。
  • 複数回インスタンス化できる。
  • 新しいインテントに応答するときには必ず新しいインスタンスが作成される。

"singleTop"

  • インテントを開始した(startActivity() を呼び出した)タスクに保持される。
  • 複数回インスタンス化できる。
  • 既存のインスタンスがスタックの最上位にあれば再利用して新しいインテントを処理する。最上位にない場合は新しいインスタンスが作成される。

"singleTask"

  • アクティビティが常にタスクのルート アクティビティになる。
  • アクティビティのインスタンスは1つに制限される。
  • 同じタスクに属する別のアクティビティを開始することができる。
  • このアクティビティがスタックの最上位にない場合インテントはドロップされる。インテントがドロップされたとしても、タスクがフォアグラウンドに移ったままになる。

"singleInstance"

  • アクティビティが常にタスクのルート アクティビティになる。
  • アクティビティのインスタンスは1つに制限される。
  • そのタスク内の唯一のアクティビティとして単独で動作。ここから別のアクティビティを開始した場合、そのアクティビティは別のタスクで起動する。

後述するが、アプリケーションのメインアクティビティには singleTask が向いているはずだが、実際は上記のような挙動にならないため standard に設定しておくと良い。

スタックのクリア

ユーザーがタスクを長時間放置したときは、ルート以外のアクティビティがクリアされる。この挙動はactivity要素の属性で変更できる。

alwaysRetainTaskState = "true"

アクティビティはクリアされない。

clearTaskOnLaunch = "true"

タスクを離れるとルートを含めた全てのアクティビティがクリアされる。

finishOnTaskLaunch = "true"

タスクを離れるとこのアクティビティはクリアされる。それがルートであっても。

Intent に FLAG_ACTIVITY_CLEAR_TOP フラグを設定すると、インテントを処理するアクティビティのインスタンスが対象タスクのスタック内に存在する場合、そのインスタンスより上位(新しい方)のアクティビティはすべてクリアされる。

startActivityForResult() で呼び出したアクティビティがクリアされてしまった場合、呼び出し元アクティビティの onActivityResult() が resultCode = RESULT_CANCELED で呼ばれる。

タスクの開始

アクティビティのインテントフィルタのアクションを android.intent.action.MAIN、カテゴリを android.intent.category.LAUNCHER に設定すると、アクティビティのアイコンとラベルがアプリケーションランチャに表示され、アプリケーションのエントリポイントになる。

ドキュメントでは、この設定をしたアクティビティはユーザーが他のタスクに移動したあとでアプリケーションに戻ってこられるように、起動モード singleTask か singleInstance を設定したほうが良いとされているが、実際にやってみると起動モード singleTask ではルートアクティビティ以外がクリアされた状態で起動してしまう。なぜか起動モード standard のタスクではスタックが保持されたまま元のタスクに戻ることができた(Android 1.6, 2.2で検証)。

参考ページ

Y.A.M の 雑記帳 「Android Activity, Task, Stack, Launch mode」