HOME > ブログ > Google App Engineでone-to-many(1対多)1kindとListProperty()で

Google App Engineでone-to-many(1対多)1kindとListProperty()で

「ListProperty()」を用いて1つのkindでone-to-many(1対多)のCRUDを作成してみる。

ケースは「Google App Engineでone-to-many(1対多)2kindとReferenceProperty()で」と同様、人物が複数のメールアドレスを持っている関係を表すこととする。
「ListProperty()」には特定の型のリストを格納することができます。
今回はメールアドレスですので、文字列「str」をしています。「item_type」は省略できるようなので、「db.ListProperty(str)」と「db.ListProperty(item_type=str)」とは同義です。

app.yaml
application: jinling-ren
version: 1
runtime: python
api_version: 1

handlers:
- url: .*
  script: main.py


main.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# one-to-manyサンプル
# http://www.jinlingren.com/

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
import person


class Root(webapp.RequestHandler):
  def get(self):
    self.redirect('/person/list')


application = webapp.WSGIApplication([
  ('/', Root),
  ('/person/list', person.PersonList),
  ('/person/view/(\w+)', person.PersonView),
  ('/person/add', person.PersonAdd),
  ('/person/edit/(\w+)', person.PersonEdit),
  ('/person/delete/(\w+)', person.PersonDelete)
],debug=True)

def main():
  run_wsgi_app(application)


if __name__ == '__main__':
  main()


model.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# one-to-manyサンプル
# http://www.jinlingren.com/

from google.appengine.ext import db

class Person(db.Model):
  name = db.StringProperty(required=True)
  mailaddress = db.ListProperty(item_type=str)
  modified = db.DateTimeProperty(auto_now=True)
  created = db.DateTimeProperty(auto_now_add=True)

  def get_sorted_mailaddress(self):
    self.mailaddress.sort()
    return self.mailaddress


person.py
#!/usr/bin/env python
#!-*- coding:utf-8 -*-
# one-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

class PersonList(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 PersonView(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 PersonAdd(webapp.RequestHandler):
  def get(self):
    html = template.render(
      os.path.join(os.path.dirname(__file__), 'template', 'person', 'add.html'),
      {}
    )
    self.response.out.write(html)
  def post(self):
    try:
      name = cgi.escape(self.request.get('name'))
      person = Person(name=name)
      mailaddress = cgi.escape(self.request.get('mailaddress'))
      if mailaddress:
        person.mailaddress.append(mailaddress)
      person.put()
      self.redirect('/person/list')
    except:
      self.redirect('/person/add')

class PersonEdit(webapp.RequestHandler):
  def get(self, key):
    person = db.get(db.Key(key))
    html = template.render(
      os.path.join(os.path.dirname(__file__), 'template', 'person', 'edit.html'),
      {
        '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'))
      email = []
      if person.mailaddress:
        seq = range(len(person.mailaddress))
        for x in seq:
          mailaddress = cgi.escape(self.request.get('mailaddress_'+str(x)))
          if mailaddress:
            email.append(mailaddress)
            logging.info(email)
      mailaddress_new = cgi.escape(self.request.get('mailaddress_new'))
      if mailaddress_new:
        email.append(mailaddress_new)
      person.mailaddress = email
      person.put()
      self.redirect('/person/list')
    except:
      self.redirect('/person/edit/'+key)

class PersonDelete(webapp.RequestHandler):
  def get(self, key):
    try:
      person = db.get(db.Key(key))
      if person:
        person.delete()
      self.redirect('/person/list')
    except:
      self.redirect('/person/list')


template/person/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>one-to-manyサンプル</title>
</head>
<body>
<h1>PersonList(人物一覧)</h1>
> <a href="/person/add">人物追加</a><br><br>

<table cellspacing="1" cellpadding="8" border="0" bgcolor="#999999">
  <tr bgcolor="#EBEBEB">
    <td>&nbsp;</td>
    <td align="center">名前</td>
    <td align="center">メールアドレス</td>
    <td>&nbsp;</td>
    <td>&nbsp;</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 mailaddress in person.get_sorted_mailaddress %}<li>{{ mailaddress }}</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/person/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>one-to-manyサンプル</title>
</head>
<body>
<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 mailaddress in person.get_sorted_mailaddress %}<li>{{ mailaddress }}</li>{% endfor %}</ul></td>
  </tr>
</table>

</body>
</html>


template/person/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>one-to-manyサンプル</title>
</head>
<body>
<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"><input type="text" name="mailaddress" value=""></td>
  </tr>
</table><br>
<input type="submit" value="追加する">
</form>

<br>
<font color="#FF0000">*</font>は必須項目

</body>
</html>


template/person/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>one-to-manyサンプル</title>
</head>
<body>
<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 mailaddress in person.get_sorted_mailaddress %}
    <input type="text" name="mailaddress_{{ forloop.counter0 }}" value="{{ mailaddress }}"><br>
{% endfor %}
    <input type="text" name="mailaddress_new" value="">
    </td>
  </tr>
</table><br>
<input type="submit" value="変更する">
</form>

<br>
<font color="#FF0000">*</font>は必須項目

</body>
</html>


「ListProperty()」を用いると、one-to-many(1対多)で1つのkindで表現できるので「2kindとReferenceProperty()」よりはすっきりしていてよりシンプルで強力なように思える。
しかし、モノは使いよう、時と場合による。

多くの方が指摘されているように、今回のように一人の人物に対してたいしてメールアドレスが付随しないようなケースでは、あまりRDBMSではやらないような非正規化をGAEデータストア設計では推奨されている。
別のモデルをたてて、データストアAPIを余計にたたくよりは小さな情報なら(時には大きな情報でも)ひとつのモデルに格納するのがベストなようだ。

ただし、ものごとにはケースバイケースがあって、この「ListProperty()」を使うとデメリットもある場合も確実に出てくるようなので注意したい。


補足:
メールアドレスのリストはメールアドレスを昇順で出している。モデル「Person」に関数「get_sorted_mailaddress」を新しく作り、メールアドレス格納リストをソートをかけたものを返している。

前回「Google App Engineでone-to-many(1対多)2kindとReferenceProperty()で」なら
  def get_sorted_mailaddress(self):
    return self.email_set.order('mailaddress')
の関数を追加して「email_set」を「get_sorted_mailaddress」に変更すればOK。

恐らくこうやって表示するたびにソートをかけるのではなく、永続化する際にソートをかけて保存する方が合理的だろう。
表示する時にこういう風になにか処理をかけて出すのは?というのを備忘録として残したいということであえての形ということでご了承くさい。
| Google App Engine | Comment:0 |
コメント投稿












画像リロード
*半角の小英字、数字で構成されています