Hatena::ブログ(Diary)

暗号、数学、時々プログラミング このページをアンテナに追加 RSSフィード

2008-04-17

Datastore に泣いたアナタに捧ぐ (中編?-1) - Google App Engine

| 07:12 | Datastore に泣いたアナタに捧ぐ (中編?-1) - Google App Engineを含むブックマーク

いわゆる「まとめサイト」系をふらふらと回ってみて、少し気付いたことがある。

1.一部の記事の存在に気づけてもらっておらず、飛び飛びでリンクがはられている場合がある
2.記事によっては、言及されているトピックに誤解があるよう。例えば2つのトピックに触れているのに1つだけ訳したものと理解されてしまっている場合アリ
3.どうやら自分以外にも翻訳を始めた人がいるみたい。ガンバレ!

1番目の問題については、このブログそのものにアピール力が足りないので、新着記事を上手く拾ってもらうのが難しい以上、今はどうしようもないだろう。

2番目の問題については、やっぱり1つのエントリーが長すぎて「読む気にならん!」って人が多くいる証なんだろうなぁ、、と。記事を複数のエントリーに分散させると読んでもらえない可能性が高い、ってのを気にして出来る限りひとまとめにしてきたが、もう少し「おもてなし」を考えた構成にしてみたいと思う。

3番目については問題でもなんでもなくて、本当に頑張って欲しいと思う。自分のはあくまで超訳なので、興味を持ってもらったり、ドキュメントのどの辺にどんなトピックがあるのか?を見る分には良いと思うものの、やはり厳密性には欠けるので。極めたいと思う人達に「次のステップ」が用意されているというのは凄く良いことだと思う。心から応援したいと思う。(例えばココとか)

そんなワケで、これからは基本的に1エントリー1トピックの方針で作成していく予定。本当は過去分もそうしたいのだが、はてなダイアリーの仕様を良く理解していないので、はてな記法を使って複数エントリーに分散させるってのが問題を起こす可能性を否定できない。残念だが今のところは自粛しようと思う。(過去分についてはとりあえず、はてな記法を使ってソースコードを読みやすくなるように編集だけしておいた)

さて、Datastore API まわりのトピックもいよいよ追い込み。少しペースを上げていく為に、ノリも鬼教官風で一気に片付けようかな、と。今日のネタはDatastore API の中の

についての超訳。Aについてを本エントリーで、Bについてを引き続いてのエントリーで紹介していきたい。


更なる一歩。覚悟はいいか?


最近調子はドウだ? 何?「少し分かってきたような気がします」だと? 間違いなくそれは気のせいだ。分かった気になった程度で満足するな。知っておくべき事は山ほどある。そして覚えたダケでは何にもならん。どんどん手を動かすようにしろ。

よし。じゃあ、さっそく始めるぞ。


クエリインデックス


全てのdatastore クエリインデックスを使う。このインデックスというのは、クエリの実行結果を並べた表みたいなものだ。App Engineアプリインデックスをindex.yaml という設定ファイル名で規定するようになってる。開発用Webサーバの場合、インデックスファイルに登録されていないクエリに出くわしたところで自動的ソイツを追加するような仕組みになってるんだが、プログラム作成側が手動で編集する事もできるぞ。但しアプリケーションアップロードする前、という条件がつくがな。

インデックスをベースにしたクエリメカニズムは一般的な多くの場合で有効だ。ただし、App Engine は他のデータベース技術では一般的とされるいくつかのクエリをサポートしていない。ここでは、それらの制限やメカニズムの中身についてみていくぞ。

流れとしては次の通りだ。

用意は良いか? 心配するな。今日の話は大して難しくないはずだ。昨日で既に峠は越しているからな。


クエリについて


クエリとは指定される条件に合ったエンティティをdatastoreから取り出す際に利用するものだ。そしてクエリではエンティティの種別やエンティティのプロパティの値に基づいた条件(これを使う場合、フィルタなんて呼ばれる)、ソート順等を指定することが出来る。クエリが実行された際、与えられた条件に合致したエンティティを全てとってくるようになってる。もちろん、指定されたソート順でな。

Datastoreでクエリを利用するには2種類の方法がある事は既に学んだな? そう、QueryインターフェイスとGqlQuery インターフェイスだ。つまり、Queryクラスのインスタンスメソッドを使う方法と、SQLライクなクエリ言語GQLを使う方法の2つになるな。これらの詳しい説明については 先日のエントリで既に確認済みと見なして先に進めるぞ。

以下はコードのサンプルだ。ポイントを忘れているようなら確認しておけ。

class Person(db.Model):
 first_name = db.StringProperty()
 last_name = db.StringProperty()
 city = db.StringProperty()
 birth_year = db.IntegerProperty()
 height = db.IntegerProperty()

# Query インターフェイス:インスタンスメソッドを使用
q = Person.all()
q.filter("last_name =", "Smith")
q.filter("height <", 72)
q.order("-height")

# GqlQuery インターフェイス:GQLクエリストリングを使用
q = db.GqlQuery("SELECT * FROM Person " +
               "WHERE last_name = :1 AND height < :2 " +
               "ORDER BY height DESC",
               "Smith", 72)

# 結果へのアクセスが実施されない限りクエリは実行されない
results = q.fetch(5)
for p in results:
 print "%s %s, %d inches tall" % (p.first_name, p.last_name, p.height)

インデックスについて


App Engine のDatastore はアプリケーションが使う可能性のある、あらゆるクエリに対応するようにインデックスをメンテしている。アプリがdatastore上のエンティティに対して変更を行った場合、それにあわせてdatastoreはインデックスを更新する、という事だ。そしてアプリクエリを実行した場合、datastoreは関連するインデックスから結果を引っ張ってくるワケだな。

アプリクエリにて用いられる種別、フィルタ条件、ソート順といったそれぞれの条件に合わせたインデックスを持っている。上のクエリの説明で出てきた例を使いながら進めるぞ。

SELECT * FROM Person WHERE last_name = "Smith"
                      AND height < 72
                    ORDER BY height DESC

このクエリに対するインデックスは種別が"Person"であるエンティティのキーをまとめたテーブル(表)になる。そして、このテーブルは"height(身長)"と"last_name"のプロパティ値を格納するための列を持っていると考えられるな。ここでインデックスは"height"プロパティの値を用いて降順にソートされている状態だ。

次に、形式は同じだがフィルタリング条件が異なる2つのクエリが同じインデックスを利用する場合について見ていくことにする。例えば以下のようなクエリは上記のクエリとそのような関係を持つ、というのは分かるな?

SELECT * FROM Person WHERE last_name = "Jones"
                       AND height < 63
                     ORDER BY height DESC

まず、datastore がクエリを実行する場合は以下のような手順となる事を覚えておけ。

1.datastore はクエリが指定する条件(種別、フィルタ条件、ソート順)に対応したインデックスを確認する
2.datastore はクエリが指定する全てのフィルタ条件に合致した最初のエンティティからインデックスのスキャンを開始する
3.datastore はフィルタ条件に合致しないエンティティが現われるまで(あるいはインデックスの終端に到達するまで)インデックスのスキャンを継続する。

ここでインデックステーブルはフィルタ、或いはソートに用いられる全てのプロパティ用の列を持っている。そして、各行は次のような条件でソートされる。

こうしておくことで、このインデックスを用いて指定され得るクエリ全ての結果を網羅したテーブルが出来上がるワケだ。

この仕組みは幅広いクエリに対して適用されるもので、殆どのアプリに対して有効だ。しかし、他のデータベースでは利用できているいくつかのクエリがサポートされていない点には注意が必要だぞ。後に続く「クエリに関する制限事項」のセクションで詳細を説明するから、しっかりと確認するようにな。

小技: クエリはフィルタ条件として文字列の一部分だけを取り出したマッチングは指定出来ない。しかし、不等号フィルタを使う事で接頭辞(Prefix)を使った似非マッチングを行う事は出来る。

db.GqlQuery("SELECT * FROM MyModel WHERE prop >= :1 AND prop < :2", "abc", "abc" + "\xEF\xBF\xBD")

この場合、値がabcで始まる文字列プロパティ"prop"を持った全てのMyModelエンティティにマッチする。バイト列である"\xEF\xBF\xBD"が意味するのは Unicodeキャラクタの理論上の最大値である。インデックス中でプロパティ値がソートされている場合、この指定条件で与えられた範囲に納まるのは、与えられた接頭辞で始まる値全てということになる。

index.yaml を用いたインデックスの定義


実は、App Engine が生成するインデックスは、デフォルトではいくつかの単純なクエリに限られている。つまり、その他のクエリを使いたい場合はアプリ側でインデックスを規定しておく必要がある。それに必要となるのがindex.yaml という名の設定ファイルだ。App Engine 上でインデックスの存在しないクエリを実行しようとした場合、それは失敗する事になる。ココは重要だぞ。しっかり理解しろ。

まず、App Engine では以下のクエリについては自動的にインデックスを生成する。

その他については index.yaml の中で規定する必要がある。例えば以下のようなものだ。

開発用Webサーバ(dev_appserver.py) を使えばindex.yaml の取り扱いは簡単だ。インデックスが用意されていないクエリが実行された場合にエラーとする代わりに、開発用Webサーバではクエリが正しく実行できるようにインデックスを新しく追加する機能を持っているからな。

つまり、ローカルPC環境において使われ得る全てのクエリの動作検証が行われていれば、アプリは既に完全なインデックスを作れている、という事になるワケだ。これは便利な機能だぞ。いちいち編集するとなると面倒だからな。まぁ、確かに試験では全てのパターンを網羅出来ていないことだってあり得るかも知れん。そういう時は、アップロード前にきちんとファイルをチェックしておいた方が良いだろうな。

小技: dev_appserver.py コマンドが「--require_indexes」オプションを共に実行された場合、index.yamlの生成は不可能となる。つまり、App Engine へのアップロードの前にこのオプションを使うと、App Engine と同じ環境を作り出すことができ、既に必要なインデックスが揃っているかどうかをチェックできる。

index.yaml はあらゆるインデックステーブル(種別、フィルタリングに必要なプロパティソート順などなど)を規定しているぞ。勿論、ここにはancestorを指定するクエリ(Query.ancestor() or a GQL ANCESTOR IS)も含まれる。使われる使われないはおいておいて、だ。そしてプロパティソートされる順番で並べられる。この場合、等号フィルタで用いられるプロパティが先に来ることなり、続いて不等号フィルタで使われるプロパティが配置され、最後にソート結果とその方向(昇順/降順)が配置されることとなる。

もう一度以下のクエリについて考えてみるぞ。

SELECT * FROM Person WHERE last_name = "Smith"
                       AND height < 72
                     ORDER BY height DESC

仮にアプリが実行するクエリがこの1つのみである場合(他にあったとしても、""Smith"と72"が置き換わる程度)、index.yaml は以下のようになるだろう。

indexes:
- kind: Person
  properties:
  - name: last_name
  - name: height
    direction: desc

エンティティが生成されたり更新されたタイミングで、全ての関連するインデックスは更新される。つまり、エンティティに関連付けられたインデックスの数に応じて、エンティティの生成や更新の処理時間は影響されるってコトになるな。やみくもにインデックスを沢山用意すれば良いってもんでもないってコトだ。

index.yaml についてもっと良く知りたい場合はココを確認しておけ。


クエリに関する制限事項


冒頭でも少し触れたように、インデックスクエリメカニズムには、いくつかの制約事項がある。ここではそれを確認していくぞ。


プロパティにおけるフィルタリングやソーティングをする際には、プロパティには値が存在していなければならない

datastoreエンティティそのものの条件としては、プロパティ値をエンティティが持つ事を必須としていない。しかし、フィルタリングプロパティを条件に指定した場合は、値を持ったエンティティのみがその対象となり、値を持たないエンティティについては無視される。


プロパティ自身を持たないエンティティをフィルタリングする事はできない

プロパティが存在していないエンティティをクエリの対象とはできない。代替策としては、値"None"をもった固定のプロパティを生成することが考えられる。値が"None"であるエンティティとしてフィルタリングすれば良いだろう。


不等号フィルタは1つのプロパティに対してのみ使用できる

クエリとしては不等号フィルタ(<, <=, >=, and >) のみを使う場合だってあり得る。

例えば以下のようなGQLは許容される。

SELECT * FROM Person WHERE birth_year >= :min
                       AND birth_year <= :max

しかし、次のようなGQLは許容されない。これは同じクエリの中で2つの異なるプロパティに対して不等号フィルタを使っている為だ。

SELECT * FROM Person WHERE birth_year >= :min_year
                       AND height >= :min_height     # ココが駄目
||< 

同じクエリの中で異なるプロパティに対して等号を使った比較を行う事は許容される。具体的には以下のようなケースだな。

>||
SELECT * FROM Person WHERE last_name = :last_name
                       AND city = :city
                       AND birth_year >= :min_year

クエリメカニズムインデックステーブルにおいて全てのクエリの結果が互いに隣接しているという事に依存しているからな。これはテーブル全てをまるごとスキャンするような事を避ける為には必要な取り決めだ。パフォーマンスを考えての話だな。言い換えれば、単一のインデックステーブルでは、複数のプロパティに対する複数の不等号フィルタリングを行えないということから、上記のような制限が生まれているというワケだ。


不等号フィルタが実施されたプロパティは他のプロパティソート前にソートされていなければならない

クエリが不等号比較と1つ以上のソート命令とを含む場合、クエリは不等号フィルタで用いられたプロパティソート命令を含まなければならない。そして、そのソート命令は他のプロパティソート命令よりも先になければならない。

したがって以下のGQLは許容されない。何故だかわかるか?フィルタリングしたプロパティソートされていないからだな。

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name              # ココが駄目

同様に以下のGQLも非許容だ。フィルタリングしたプロパティよりも先に他のプロパティソートが行われてしまうからな。

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name, birth_year  # ココが駄目

これはOKだ。

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY birth_year, last_name

不等号フィルタにマッチする全ての結果を得るためには、クエリインデックステーブルで最初にマッチした行からスキャニングを始めることになる。それはマッチしない行が発見されるまで続けれられる事になるから、不整合が出ないようにするにはこの順番が大事になる、というコトだな。気をつけるように。


Not-Equal(!=)フィルタは非サポート

困った事にdatastoreは現在、フィルタリング条件としてnot-equal(!=)をサポートしていない。将来リリースでは対応するかも、とGoogleは言っているがな。カモ、だから期待はしない方が良いかも知れんな。


【次エントリー:トランザクション】へ続く…