Google App Engineでmany-to-many(多対多)2kindと接続用モデルで
2010/05/07 09:35:00
「many-to-many(多対多)」をGAE上で表現してみる。ネット上にこの手法の記事が少ないことからあまりメジャーなやり方でないかもしれないが、確実に言えることは接続用モデルを作ることで1kind増えるので関連データを引っ張ってくるのにデータストアの呼び出し回数が増える=パフォーマンスに影響するということ。
それを踏まえた上で参考までに。
app.yaml
main.py
model.py
circle.py
person.py
template/circle/list.html
template/circle/view.html
template/circle/add.html
template/circle/edit.html
template/circle/list.html
template/circle/view.html
template/circle/add.html
template/circle/edit.html
もうこれくらいになってくるとしんどくなってきているのがよくわかる。
それぞれの処理ファイルは高々100行たらずのコードだが、何度も同じようなところがあるのがよくわかる。
削除の際に関連モデルのデータもループして削除しているが、この処理もモデルこそ違えど「人物」を削除する時も「サークル」を削除する時も処理は同じだ。
処理が同じならまとめて使いまわすべきだろう。
また削除も回して1件ずつdeleteしているがdeleteにはキーのリストをくわせて一括で削除もできるみたいなのでそういう処理をしてデータストアAPIをたたく回数を極力少なくするという工夫も必要だと思う。(正直それで回数が減るかは確認していなけど)
とにかくわかってきたのは、このようなソースの作り方ではなくよりコードを最適化する手法に変えていく必要があるということだ。
現状のやり方はいちアクセスの処理をget、postに単にわけて処理をトレースさせて次画面に遷移するという単純なものだが、よりデータの抽出や挿入、削除といった箇所を関数化してスリム化する余地があるということに気づかせてくれる。
application: jinling-ren version: 1 runtime: python api_version: 1 handlers: - url: .* script: main.py
main.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# many-to-manyサンプル
# http://www.jinlingren.com/
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
import person
import circle
class Root(webapp.RequestHandler):
def get(self):
self.redirect('/circle/list')
application = webapp.WSGIApplication([
('/', Root),
('/person/list', person.List),
('/person/view/([\w\-]+)', person.View),
('/person/add', person.Add),
('/person/edit/([\w\-]+)', person.Edit),
('/person/delete/([\w\-]+)', person.Delete),
('/circle/list', circle.List),
('/circle/view/([\w\-]+)', circle.View),
('/circle/add', circle.Add),
('/circle/edit/([\w\-]+)', circle.Edit),
('/circle/delete/([\w\-]+)', circle.Delete)
],debug=True)
def main():
run_wsgi_app(application)
if __name__ == '__main__':
main()
model.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# many-to-manyサンプル
# http://www.jinlingren.com/
from google.appengine.ext import db
class Person(db.Model):
name = db.StringProperty(required=True)
modified = db.DateTimeProperty(auto_now=True)
created = db.DateTimeProperty(auto_now_add=True)
def circles(self):
return (x.circle for x in self.personcircle_set)
class Circle(db.Model):
name = db.StringProperty(required=True)
modified = db.DateTimeProperty(auto_now=True)
created = db.DateTimeProperty(auto_now_add=True)
def persons(self):
return (x.person for x in self.personcircle_set)
class PersonCircle(db.Model):
person = db.ReferenceProperty(reference_class=Person, required=True)
circle = db.ReferenceProperty(reference_class=Circle, required=True)
circle.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# many-to-manyサンプル
# http://www.jinlingren.com/
from google.appengine.ext import webapp, db
from google.appengine.ext.webapp import template
import os
import cgi
import logging
from model import Person, Circle
class List(webapp.RequestHandler):
def get(self):
circles = Circle.all().order('name')
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'circle', 'list.html'),
{
'circles': circles
}
)
self.response.out.write(html)
class View(webapp.RequestHandler):
def get(self, key):
circle = db.get(db.Key(key))
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'circle', 'view.html'),
{
'circle': circle
}
)
self.response.out.write(html)
class Add(webapp.RequestHandler):
def get(self):
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'circle', 'add.html'),
{}
)
self.response.out.write(html)
def post(self):
try:
name = cgi.escape(self.request.get('name'))
circle = Circle(name=name)
circle.put()
self.redirect('/circle/list')
except:
self.redirect('/circle/add')
class Edit(webapp.RequestHandler):
def get(self, key):
circle = db.get(db.Key(key))
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'circle', 'edit.html'),
{
'circle': circle
}
)
self.response.out.write(html)
def post(self, key):
try:
circle = db.get(db.Key(key))
circle.name = cgi.escape(self.request.get('name'))
circle.put()
self.redirect('/circle/list')
except:
self.redirect('/circle/edit/'+key)
class Delete(webapp.RequestHandler):
def get(self, key):
try:
circle = db.get(db.Key(key))
if circle:
for personcircle in circle.personcircle_set:
personcircle.delete()
circle.delete()
self.redirect('/circle/list')
except:
self.redirect('/circle/list')
person.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# many-to-manyサンプル
# http://www.jinlingren.com/
from google.appengine.ext import webapp, db
from google.appengine.ext.webapp import template
import os
import cgi
import logging
from model import Person, Circle, PersonCircle
class List(webapp.RequestHandler):
def get(self):
persons = Person.all().order('name')
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'person', 'list.html'),
{
'persons': persons
}
)
self.response.out.write(html)
class View(webapp.RequestHandler):
def get(self, key):
person = db.get(db.Key(key))
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'person', 'view.html'),
{
'person': person
}
)
self.response.out.write(html)
class Add(webapp.RequestHandler):
def get(self):
circles = Circle.all().order('name')
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'person', 'add.html'),
{
'circles': circles
}
)
self.response.out.write(html)
def post(self):
try:
name = cgi.escape(self.request.get('name'))
person = Person(name=name)
person.put()
circle_arr = self.request.get_all('circle_keys')
for key in circle_arr:
circle = db.get(db.Key(key))
personcircle = PersonCircle(person=person, circle=circle)
personcircle.put()
self.redirect('/person/list')
except:
self.redirect('/person/add')
class Edit(webapp.RequestHandler):
def get(self, key):
circles = Circle.all().order('name')
person = db.get(db.Key(key))
def mapit(circle):
#isTrue = PersonCircle.all().filter('person =', person.key()).filter('circle =', circle.key()).get()
isTrue = person.personcircle_set.filter('circle =', circle.key()).get()
val = {
'key': circle.key(),
'name': circle.name,
'select': isTrue
}
return val
circles = map(mapit,circles)
html = template.render(
os.path.join(os.path.dirname(__file__), 'template', 'person', 'edit.html'),
{
'circles': circles,
'person': person
}
)
self.response.out.write(html)
def post(self, key):
try:
person = db.get(db.Key(key))
person.name = cgi.escape(self.request.get('name'))
person.put()
circle_arr = self.request.get_all('circle_keys')
for personcircle in person.personcircle_set:
personcircle.delete()
for key in circle_arr:
circle = db.get(db.Key(key))
personcircle = PersonCircle(person=person, circle=circle)
personcircle.put()
self.redirect('/person/list')
except:
self.redirect('/person/edit/'+key)
class Delete(webapp.RequestHandler):
def get(self, key):
try:
person = db.get(db.Key(key))
if person:
for personcircle in person.personcircle_set:
personcircle.delete()
person.delete()
self.redirect('/person/list')
except:
self.redirect('/person/list')
template/circle/list.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/person/list">人物一覧</a><br><br>
<h1>List(サークル一覧)</h1>
> <a href="/circle/add">サークル追加</a><br><br>
<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr bgcolor="#EBEBEB">
<td> </td>
<td align="center">名前</td>
<td align="center">所属人物</td>
<td> </td>
<td> </td>
</tr>
{% for circle in circles %}
<tr bgcolor="#FFFFFF">
<td><a href="/circle/view/{{ circle.key }}">詳細</a></td>
<td>{{ circle.name }}</td>
<td><ul>{% for person in circle.persons %}<li>{{ person.name }}</li>{% endfor %}</ul></td>
<td><a href="/circle/edit/{{ circle.key }}">編集</a></td>
<td><a href="/circle/delete/{{ circle.key }}" onClick="if(confirm('本当に削除しますか?')){return true;}else{return false;}">削除</a></td>
</tr>
{% endfor %}
</table>
</body>
</html>
template/circle/view.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/person/list">人物一覧</a><br><br>
<h1>PersonView(サークル参照)</h1>
> <a href="/circle/list">サークル一覧</a><br><br>
<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr>
<td width="100" bgcolor="#EBEBEB">名前</td>
<td width="250" bgcolor="#FFFFFF">{{ circle.name }}</td>
</tr>
<tr>
<td bgcolor="#EBEBEB">所属人物</td>
<td bgcolor="#FFFFFF"></td>
</tr>
</table>
</body>
</html>
template/circle/add.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/person/list">人物一覧</a><br><br>
<h1>PersonAdd(サークル追加)</h1>
> <a href="/circle/list">サークル一覧</a><br><br>
<form action="/circle/add" method="post">
<table border="0" cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr>
<td width="100" bgcolor="#EBEBEB">名前 <sup><font color="#FF0000">*</font></sup></td>
<td width="250" bgcolor="#FFFFFF"><input type="text" name="name" value=""></td>
</tr>
</table><br>
<input type="submit" value="追加する">
</form>
<br>
<font color="#FF0000">*</font>は必須項目
</body>
</html>
template/circle/edit.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/person/list">人物一覧</a><br><br>
<h1>PersonEdit(サークル編集)</h1>
> <a href="/circle/list">サークル一覧</a><br><br>
<form action="/circle/edit/{{ circle.key }}" method="post">
<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr>
<td width="100" bgcolor="#EBEBEB">名前 <sup><font color="#FF0000">*</font></sup></td>
<td width="250" bgcolor="#FFFFFF"><input type="text" name="name" value="{{ circle.name }}"></td>
</tr>
</table><br>
<input type="submit" value="変更する">
</form>
<br>
<font color="#FF0000">*</font>は必須項目
</body>
</html>
template/circle/list.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/circle/list">サークル一覧</a><br><br>
<h1>PersonList(人物一覧)</h1>
> <a href="/person/add">人物追加</a><br><br>
<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr bgcolor="#EBEBEB">
<td> </td>
<td align="center">名前</td>
<td align="center">所属サークル</td>
<td> </td>
<td> </td>
</tr>
{% for person in persons %}
<tr bgcolor="#FFFFFF">
<td><a href="/person/view/{{ person.key }}">詳細</a></td>
<td>{{ person.name }}</td>
<td><ul>{% for circle in person.circles %}<li>{{ circle.name }}</li>{% endfor %}</ul></td>
<td><a href="/person/edit/{{ person.key }}">編集</a></td>
<td><a href="/person/delete/{{ person.key }}" onClick="if(confirm('本当に削除しますか?')){return true;}else{return false;}">削除</a></td>
</tr>
{% endfor %}
</table>
</body>
</html>
template/circle/view.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/circle/list">サークル一覧</a><br><br>
<h1>PersonView(人物参照)</h1>
> <a href="/person/list">人物一覧</a><br><br>
<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr>
<td width="100" bgcolor="#EBEBEB">名前</td>
<td width="250" bgcolor="#FFFFFF">{{ person.name }}</td>
</tr>
<tr>
<td bgcolor="#EBEBEB">所属サークル</td>
<td bgcolor="#FFFFFF"><ul>{% for circle in person.circles %}<li>{{ circle.name }}</li>{% endfor %}</ul></td>
</tr>
</table>
</body>
</html>
template/circle/add.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/circle/list">サークル一覧</a><br><br>
<h1>PersonAdd(人物追加)</h1>
> <a href="/person/list">人物一覧</a><br><br>
<form action="/person/add" method="post">
<table border="0" cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr>
<td width="100" bgcolor="#EBEBEB">名前 <sup><font color="#FF0000">*</font></sup></td>
<td width="250" bgcolor="#FFFFFF"><input type="text" name="name" value=""></td>
</tr>
<tr>
<td bgcolor="#EBEBEB">所属サークル</td>
<td bgcolor="#FFFFFF">
{% for circle in circles %}
<input type="checkbox" name="circle_keys" value="{{ circle.key }}">{{ circle.name }}<br>
{% endfor %}
</td>
</tr>
</table><br>
<input type="submit" value="追加する">
</form>
<br>
<font color="#FF0000">*</font>は必須項目
</body>
</html>
template/circle/edit.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>many-to-manyサンプル</title>
</head>
<body>
> <a href="/circle/list">サークル一覧</a><br><br>
<h1>PersonEdit(人物編集)</h1>
> <a href="/person/list">人物一覧</a><br><br>
<form action="/person/edit/{{ person.key }}" method="post">
<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
<tr>
<td width="100" bgcolor="#EBEBEB">名前 <sup><font color="#FF0000">*</font></sup></td>
<td width="250" bgcolor="#FFFFFF"><input type="text" name="name" value="{{ person.name }}"></td>
</tr>
<tr>
<td bgcolor="#EBEBEB">メールアドレス</td>
<td bgcolor="#FFFFFF">
{% for circle in circles %}
<input type="checkbox" name="circle_keys" value="{{ circle.key }}"{% if circle.select %} checked="checked"{% endif %}>{{ circle.name }}<br>
{% endfor %}
</td>
</tr>
</table><br>
<input type="submit" value="変更する">
</form>
<br>
<font color="#FF0000">*</font>は必須項目
</body>
</html>
もうこれくらいになってくるとしんどくなってきているのがよくわかる。
それぞれの処理ファイルは高々100行たらずのコードだが、何度も同じようなところがあるのがよくわかる。
削除の際に関連モデルのデータもループして削除しているが、この処理もモデルこそ違えど「人物」を削除する時も「サークル」を削除する時も処理は同じだ。
処理が同じならまとめて使いまわすべきだろう。
また削除も回して1件ずつdeleteしているがdeleteにはキーのリストをくわせて一括で削除もできるみたいなのでそういう処理をしてデータストアAPIをたたく回数を極力少なくするという工夫も必要だと思う。(正直それで回数が減るかは確認していなけど)
とにかくわかってきたのは、このようなソースの作り方ではなくよりコードを最適化する手法に変えていく必要があるということだ。
現状のやり方はいちアクセスの処理をget、postに単にわけて処理をトレースさせて次画面に遷移するという単純なものだが、よりデータの抽出や挿入、削除といった箇所を関数化してスリム化する余地があるということに気づかせてくれる。