datastoreでなんとか望むデータをぐきゅるためのテクニック

まず一例

数値と生成時刻だけを持つモデルを例に取る。

def Item(db.Model):
    score   = db.IntegerProperty(default=0)
    created = db.DateTimeProperty(auto_now_add=True)
ぐきゅる
#itemを取得する
qs = Item.all()
オフセット、リミットを指定して
#10番目から5個取得する
qs = Item.all().fetch(5, 10)

できないことあれこれ

1000番目以降のデータをぐきゅることはできない
#2000番目から5個取得する
qs = Item.all().fetch(5, 2000) #動かない!


同様に、

1000個以上要素があるなら総数は調べられない
print Item.all().count() #上限が1000
複数のプロパティを、それぞれイコール以外の条件を使って絞り込みできない
#scoreが100未満で、2000年以前に作成されたものを取得する
qs = Item.gql('WHERE score < :1 AND created <= :2',
              100,
              '2000-01-01 00:00:00'
              ) #動かない!


そのままだと余りに自由度がないので、これらを多少改善していく。

とりあえず、auto-incrementなidは付加しておくと便利

def Item(db.Model):
    id      = db.IntegerProperty(required=True)
    score   = db.IntegerProperty(default=0)
    created = db.DateTimeProperty(auto_now_add=True)

とモデルを定義して、

def get_max_id(db_class):
    """idの最大数を返す"""
    qs = db_class.gql('ORDER BY id DESC LIMIT 1')
    for q in qs:
        return q.id
    return 0
    
def add_item(id, score):
    """itemを追加する"""
    key = 'i%s' % id
    obj = db.get( db.Key.from_path('Item', key) )
    if not obj:
        obj = Item(id=id,
                   score=score
                   )
        obj.put()
    else:
        raise Exception

try:
    id = get_max_id(Item)
    id += 1
    db.run_in_transaction(add_item, id, score)
    
    ...

のようにすると、1から順にitemそれぞれにidが付加される。


こうすると、

1000個以上要素があっても、正しい総数が取得できる

print Item.all().count() #上限が1000


 ↓

print get_max_id(Item) #1000以上でも大丈夫


また、fetch()の制限は、
「WHEREで絞った後の結果について、1000番目以降についてはアクセスできませんよ」ということなので、max_idを使うとWHEREで任意の番号のものを絞り込むことができる。つまり、

1000番目以降のデータをぐきゅることができる

#2000番目から5個取得する
qs = Item.all().fetch(5, 2000) #動かない!


 ↓

#2000番目から5個取得する
qs = Item.gql('WHERE :1 <= id',
              2000
              ).fetch(5)

これがfetch(5, 2000)の代替としてなり立つのは、他に何もWHEREの条件がないから。
その点は注意を。

複数の、イコール以外の条件で絞り込みたいなら

よく考えないとぐだぐだになる感がひしひしとするが、

条件が固定な場合、その条件の真偽をプロパティにしておく
def Item(db.Model):
    id      = db.IntegerProperty(required=True)
    score   = db.IntegerProperty(default=0)
    created = db.DateTimeProperty(auto_now_add=True)
    score_lt_100 = BooleanProperty()

モデルに真偽値のプロパティを追加し、
scoreにアクセスするときにこの条件を判定し、常に反映させておけば、

def lt_100(score):
    if score < 100:
        return True
    return False

new_score = 200
obj = db.get( db.Key.from_path('Item', 'i%s' % id) )
if obj:
    obj.score = new_score
    obj.score_lt_100 = lt_100(new_score)
    obj.put()
else:
    raise Exception

#scoreが100未満で、2000年以前に作成されたものを取得する
qs = Item.gql('WHERE score < :1 AND created <= :2',
              100,
              '2000-01-01 00:00:00'
              ) #動かない!


 ↓

#scoreが100未満で、2000年以前に生成されたものを取得する
qs = Item.gql('WHERE score_lt_100 = :1 AND created <= :2',
              True,
              '2000-01-01 00:00:00'
              )

とできる。

例えばある月、ある週などで絞り込みたいときでも同様に、
def Item(db.Model):
    id      = db.IntegerProperty(required=True)
    score   = db.IntegerProperty(default=0)
    created = db.DateTimeProperty(auto_now_add=True)
    score_lt_100 = BooleanProperty()
    cym = db.StringProperty()
    cyW = db.StringProperty()

とモデルを定義して、

try:
    now = datetime.datetime.now()
    cym = now.strftime('%y-%m')
    cyW = now.strftime('%y-%W')
    id = get_max_id(Item)
    id += 1
    db.run_in_transaction(add_item, id, score, cym, cyW)

として追加すれば、

#今月作成された、scoreが100未満のものを取得する
qs = Item.gql('WHERE score_lt_100 = :1 AND cym = :2',
              True,
              cym
              )

とできる。
もしくは、

#今週作成された、scoreが100未満で、idが2000以降のものを、5個取得する
qs = Item.gql('WHERE score_lt_100 = :1 AND cyW = :2 AND :3 < id',
              True,
              cyW,
              2000
              ).fetch(5)

のようにも。
この場合、前述のように、このクエリはfetch(5, 2000)と等価でないことに注意。


常に反映させることは不可能な場合や、条件が変動する場合などは、cronで判定ジョブを走らせることになると思う。
その際はcreatedや参照の構造などを参考に、ジョブがチェックする場所を最適化しないと、データが多くなるにつれてしんどくなる。

ORDER BY について

http://code.google.com/intl/ja/appengine/docs/python/datastore/queriesandindexes.html
にちょこっと書いてあるが、

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name, birth_year  #エラー
SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY birth_year, last_name  #正しい

この場合、ORDER BYの先頭にbirth_yearがこないといけない。
後者の場合、index.yaml

indexes:
- kind: Person
  properties:
  - name: birth_year
  - name: last_name

になる。

SELECT * FROM Person WHERE birth_year = :min_year
                     ORDER BY last_name  #正しい

はエラーにならない。
index.yaml

indexes:
- kind: Person
  properties:
  - name: birth_year
  - name: last_name

で、前の例と同じ。


ここ半日ぐらいで調べたところなので、断言する自信はないが、

要するに、WHERE句で一つしか使えない、イコール以外で絞り込んだプロパティは、オーダーに影響するのでORDER BYに書かなきゃいけませんよ、という話(だと思う)。
    WHERE foo = :1 AND bar < :2
        ORDER BY bar, baz  #正しい

であれば、インデックスは

indexes:
- kind: Hoge
  properties:
  - name: foo
  - name: bar
  - name: baz

のようになる。

ということで

SQLが恋しい、と思いつつなんとかかんとかGAEでこのぐらいはできそう。
みんながぐきゅってくれることを期待してメモを置いておく。
れっつ、ぐきゅる!