When it’s ready.

出来るまで出来ない

GAEで簡単な多対多的なモノ

db.ReferenceProperty(db.Mode)を使って、テーブルを作る。

(http://ondras.zarovi.cz/sql/demo/)
こんな感じに、テーブルを設計した。MySQL用のER図なので、フィールドの名前が違うけどなんとなくやりたい事は通じると思う。っていうか、ワカレ。

左のTableというテーブル内のid_Coreとid_Tagが、ReferencePropertyで定義されるFKとなる。
モデルは

class Table(db.Model):
   ref_core = db.ReferenceProperty(Core)
   ref_tag = db.ReferenceProperty(Tag)
   create_at = db.DateTimeProperty(auto_now_add = True)

と言う風に書いてみた。

やりたい事

  1. 文章(以下Core)と一緒にメタデータ(以下Tag)を保存する
  2. Coreに対して、複数のTagを付けられるようにしたい
  3. 同じ内容のTagは使い回したい
  4. Tagから、Coreを検索したい

Tagを、classとしたいのは、メタデータに対して使用頻度などのメタメタデータを付加するため。また、今後Tagエンジンを追加する際にTag情報だけのDBをたてやすくする為

実装してみる

気づいてしまえば、なんでこんな簡単なことを気づかなかったのだろうと思う。1時間ほどで出来てしまった。(ユーザー情報がめんどくさかったので実装からは外した)

# main.py
# encoding:utf-8
#! /usr/bin/env python

import wsgiref.handlers
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp \
  import template

class Core(db.Model):
  body = db.StringProperty(required = True)
  create_at = db.DateTimeProperty(auto_now_add = True)

class Tag(db.Model):
  body = db.StringProperty()
  create_at = db.DateTimeProperty(auto_now_add = True)

class Table(db.Model):
   ref_core = db.ReferenceProperty(Core)
   ref_tag = db.ReferenceProperty(Tag)
   create_at = db.DateTimeProperty(auto_now_add = True)

def tagSplit(str):
  """文字列をコンマ区切りに配列で返す"""
  return str.split(',')

class index(webapp.RequestHandler):
  def get(self):
    tables = db.GqlQuery(
      'SELECT * from Table ORDER BY create_at DESC')
    cores = db.GqlQuery(
      'SELECT * from Core ORDER BY create_at DESC')
    tags = db.GqlQuery(
      'SELECT * from Tag ORDER BY create_at DESC')
    
    tableObj = Table()
    data_arr, num = [], 0

    # すべてのcoreデータで処理を回す
    for c in cores:
      tag_list = []
      # tableデータ内でcore毎にtagを取り出しtagListを作成する
      for i in tableObj.all().filter('ref_core = ', c):
        if i.ref_core.key()==c.key():
          tag_list.append(i.ref_tag)
      # coreをKeyにして、TagListをappend
      data_arr.append({
        'core' : c,
        'tag' : tag_list
      })

    values = {
      'tables' : tables,
      'cores' : cores,
      'tags' : tags,
      'core_cnt' : cores.count(),
      'tag_cnt' : tags.count(),
      'table_cnt' : tables.count(),
      'core_list' : data_arr,      
    }
    self.response.out.write(
      template.render('main.html', values))
      
  def post(self):
    # コアのみ保存する
    core = Core(body = self.request.get('core'))
    core.put()
    tt = Table()
    # 複数タグ対応
    for i in tagSplit(self.request.get('tag')):
      tag = Tag(body = i)
      tag.put()
      table = Table(
        ref_tag = tag,
        ref_core = core
        )
      # tagとtableをタグ毎にcoreと結びつけて保存する
      
      table.put()
    self.redirect('/')
    
def main():
  app = webapp.WSGIApplication([
    (r'.*', index)], debug=True)
  wsgiref.handlers.CGIHandler().run(app)
  
  
if __name__ == '__main__':
  main()
  
    
<H1>mrs test</H1>
<hr>
<form action="" method="post" accept-charset='utf-8'>
	<p>Core: <input type="text" name="core" value="" id=""></p>
	<p>Tag: <input type="text" name="tag" value="" id=""></p>
	<p><input type="submit" value="save"></p>
</form>

{% for i in core_list.values %}
{{i.core.body}}<br>
{% for ii in i.tag %}
  [{{ii.body}}]
{% endfor %}<br>
{{i.core.create_at}}
<p>
{% endfor %}
<hr>
core数:{{core_cnt}}<br>
tag数:{{tag_cnt}}<br>
table数:{{table_cnt}}<br>

見苦しくて申し訳ない。

テーブルを元にして、coreとTagを取得するとCoreの数がTable分増えてしまうので、HashのKeyにCoreのKeyを使用してCoreの数分だけのデータを生成した。

Datastoreのドキュメントには、逆参照のサンプルが載っていたがうまく動かなかった。現在は、Coreの数だけTableに総当たりで探しにいっているのでこのやり方はまずいと思う。CoreをSet型で作って、TagをAddしていけば、CoreとTableを一回ずつの走査になるかと思ったけどやり方がよくわからなかったので、泥臭いことをしてしまった。

今後の課題

  • 逆参照のやり方を身につける
  • Set型を使いこなして多対多の関連をシンプルに抜き出す
  • Tagから、使用されているCoreを逆参照を用いてfilterする
  • Tagが毎回作られているので、get_or_insertを使うようにする

追記

フィルター有無でスピード計ってみた。
開発コンソール上で計ったので正しくないかも知れない

import datetime
import wsgiref.handlers
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp \
  import template
class Core(db.Model):
  body = db.StringProperty(required = True)
  create_at = db.DateTimeProperty(auto_now_add = True)

class Tag(db.Model):
  body = db.StringProperty()
  create_at = db.DateTimeProperty(auto_now_add = True)

class Table(db.Model):
   ref_core = db.ReferenceProperty(Core)
   ref_tag = db.ReferenceProperty(Tag)
   create_at = db.DateTimeProperty(auto_now_add = True)

tim1 = datetime.datetime.now()
for h in Core.all():
  print h.body
  for i in Table.all().filter('ref_core =', h):
    print "["+i.ref_tag.body+"]"
  print ''
tim2 = datetime.datetime.now()
tim2d = tim2-tim1

for h in Core.all():
  print h.body
  for i in Table.all():
    if i.ref_core.key() == h.key():
      print "["+i.ref_tag.body+"]"
  print ''
tim3 = datetime.datetime.now()
tim3d = tim3 - tim2

print '---'
print tim2d
print tim3d
print Core.all().count()
print Table.all().count()

結果は

---
0:00:00.401738
0:00:00.781612
17
14

と言う事で、14個程度しか無いにもかかわらず倍近く違うので、やっぱ総当たりは良くないよね。
上のソースも、フィルターありに変えました。