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 {
    // アクセス拒否
}