Google App EngineとDjango

ずいぶん前のことになってしまいましたが、申し込みしてしばらく待った後、Google App Engine(GAE)のアカウントをいただいていました。せっかくアカウントを取得したので何か試してみようと思い、Djangoチュートリアルにあるサンプルを動かしてみました。が、ことのほか苦労したので忘れないようにメモです。

PythonDjangoもGAEも今回初めて使ったので、苦労するのは仕方が無いのですが、GAE特有のところはまだ情報があまり多くなくて特に苦労しました。とりわけDatastore関係です。 MySQLPostgreSQLのようなリレーショナルデータベースではなく、Django的なやり方では使えないので。Getting Started With Django  |  Python  |  Google Cloudには"the only necessary adjustment is modifying your Django data models to make use of the Google App Engine Datastore API ..."のように、いかにも通常のDjangoと違うのは少しばかり、、、のように書いてあるのですが、この違いはかなり大きいのではないか、と思いました。

とりあえず、動くようになるまでの手順を。


1. GAEのSDKをインストール(ときどき更新されてるので最新版をインストールする)


2. GAEのDjango helper(Google Code Archive - Long-term storage for Google Code Project Hosting.)をダウンロードして、適当なディレクトリに展開する
参考:Getting Started With Django  |  Python  |  Google Cloud


3. mysiteというディレクトリを作って、そこに__init__.py, app.yaml, appengine_django(ディレクトリ、以下一式), main.py, manage.py, settings.py, urls.pyを置く(ダウロードしたアーカイブを展開したときにできるappengine_helper_for_djangoをmysiteにrenameしてもいいはず。)


4. mysiteディレクトリからgoogle-appengineへのリンクを作る

ln -s /usr/local/google-appengine .google-appengine

OSXの場合、GAE SDKは/usr/local以下にインストールされる。が、SDKをアップデートしたら、実際には別のディレクトリにインストールされて/usr/localからリンクが張られていた。しかも、なぜか、リンク先で.zipファイルが展開されていなくて、最初は何も動かなくて???。zipファイルを展開したら動いた。


5. テスト用のサーバを起動する

python manage.py runserver 8080

[要調査]ポート番号を指定しないと、ディフォルトの8000でサーバが起動するが、8080を指定して起動しないとdatastoreに接続できない?。一回目に8080を指定していたので、以後、同じポート番号にしないといけなかったのかも、、、


6. 環境の(?)テスト

python manage.py test

当初、ここでFが一つ出ていて、datastore関係のところがうまくうごかなかった。
[要調査]いくつかdatastoreにデータを登録したのち、このコマンドを動かしたら中身がclearされてしまったような、、、気のせいだろうか?


7. app.yamlのapplicationの項目を修正
この名前は公開サーバで動かすときの名前になる。例えば、application: example とするとexample.appspot.comがURLになる。その他、datastoreの名前にも使われる。OSXの場合、/var/folders/xY/xYuRY(以下記号が続く)/-Tmp-/django_example.datastoreという名前で、datastoreの中身が保存されていた。ここ以外の場所に何かがあるかどうかは未調査。


8. Djangoチュートリアルにあるのと同じようにpollsアプリケーションのひな形を作る

python manage.py startapp polls


9. モデルを作る

from appengine_django.models import BaseModel
from google.appengine.ext import db

class Poll(BaseModel):
    question = db.StringProperty()
    pub_date = db.DateTimeProperty('date published')

class Choice(BaseModel):
    poll = db.ReferenceProperty(Poll)
    choice = db.StringProperty()
    votes = db.IntegerProperty()

オリジナルのDjangoチュートリアルどおりには作れない。この例はGetting Started With Django  |  Python  |  Google Cloudにあるものと同じ。

ローカルで動かしている環境ではhttp://localhost:8080/_ah/admin/を開くとEntity(データ)をadministrationツール経由で登録できるようになっているが、まったく何もない最初の状態ではリレーショナルDBのテーブルに相当するものも無いので、ツールは使えない。python manage.py shellでインタープリタを起動して、ひとつでも登録すればadministrationツールを使って登録できるようになった。これは実際の公開サーバ appspot.comにアプリケーションを登録した場合でも同様に、全く新規の状態では公開サーバのadministrationツールであるData Viewerを使っても、データを追加できない。GAEのDjango Helper場合、python manage.py syncdbとかsql*とかのコマンドは無い。

ローカルな環境のadministrationツールではPollのpub_dateはdatetime型になっていたが、Data Viewerではint型になってしまっているので、2008-8-17みたいな表記で入力できなかった。バグだろうか。


10. settings.pyにpollsを追加

(最後の部分)
INSTALLED_APPS = (
     'appengine_django',
     'django.contrib.auth',
#    'django.contrib.contenttypes',
#    'django.contrib.sessions',
#    'django.contrib.sites',
     'polls',
)

GAEの場合、app.yamlがあるディレクトリがベースになるので、相対的なパスを指定する場合、app.yamlがあるディレクトリから見たパス名を指定する。今回は、app.yamlがあるmysiteディレクトリで python manage.py startspp pollsを実行したので、 mysite以下に pollsディレクトリが作られている。このためsettings.pyのINSTALLED_APPSにはpollsだけを指定している。(Djangoチュートリアルではmysite.polls)


11. Djangoチュートリアルに従ってviewを作る--その1-- urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    # Example:
    # (r'^foo/', include('foo.urls')),
    (r'^polls/$', 'polls.views.index'),
    (r'^polls/(?P\d+)/$', 'polls.views.detail'),
    (r'^polls/(?P\d+)/results/$', 'polls.views.results'),
    (r'^polls/(?P\d+)/vote/$', 'polls.views.vote'),

    # Uncomment this for admin:
#     (r'^admin/', include('django.contrib.admin.urls')),
)

チュートリアルどおり。最後のadminのエントリはコメントを外して、必要な行をsettings.pyに追加して試してみたけれど、ダメ。やはりdata modelがまるで違うから無理なのかも。DjangoのすてきなUIが気に入っているのですが。


12. Djangoチュートリアルに従ってviewを作る--その2-- views.py
GAEのdatastore APIは独自なので、Djangoのサンプルはそのままでは動かない。idの持ち方も違う(modelのインスタンスからkeyを取得すると、そこにid()メソッドがある)ので、テンプレートも含めて修正。どうすればGAEでget_object_or_404メソッドを使えるようになるのかもわからなかったので、今回はtry, exceptを使った。

実際のサーバのData Viewerではdatetime型のはずの入力項目がint型になってしまっていて日時の入力ができないし、インタープリタモードでデータ登録もできないので、プログラム的にデータを登録する部分も追加。GQLを直接入力できるフォームはあるが、GQLはSELECTのみでUPDATEやINSERTはできないようだ。

# Create your views here.

from django.shortcuts import render_to_response, get_object_or_404
from django.http import Http404, HttpResponseRedirect
from polls.models import Poll, Choice
from datetime import datetime
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.all().order('-pub_date')[:5]
    if len(latest_poll_list) == 0:
        setup_entities()
    return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})

def setup_entities():
    p = Poll(question='What\'s up?', pub_date=datetime.now())
    p.save()
    c = Choice(poll=p, choice='Nothing.', votes=0)
    c.save()
    c = Choice(poll=p, choice='So far, so good.', votes=0)
    c.save()
    p = Poll(question='What\'re you doing?', pub_date=datetime.now())
    p.save()
    c = Choice(poll=p, choice='I\'m driving home.', votes=0)
    c.save()
    c = Choice(poll=p, choice='I\'m listening to music.', votes=0)
    c.save()
    c = Choice(poll=p, choice='I\'m googling.', votes=0)
    c.save()
    p = Poll(question='OK?', pub_date=datetime.now())
    p.save()
    c = Choice(poll=p, choice='All right.', votes=0)
    c.save()

def detail(request, poll_id):
    try:
        p = Poll.get_by_id(ids=int(poll_id))
        choices = p.choice_set.fetch(limit=100)
    except Poll.DoesNotExist:
        raise Http404
    return render_to_response('polls/detail.html', {'poll': p, 'choices': choices})

def vote(request, poll_id):
    p = Poll.get_by_id(ids=int(poll_id))
    try:
        selected_choice = Choice.get_by_id(ids=int(request.POST['choice']))
    except (KeyError, Choice.DoesNotExist):
        return render_to_response('polls/detail.html', {
            'poll': p,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect('/polls/%s/results/' % p.key().id())

def results(request, poll_id):
    p = Poll.get_by_id(ids=int(poll_id))
    choices = p.choice_set.fetch(limit=100)
    return render_to_response('polls/results.html', {'poll': p, 'choices': choices})


13. Djangoチュートリアルに従ってviewを作る--その3-- テンプレート
settings.pyに

ROOT_PATH = os.path.dirname(__file__)
TEMPLATE_DIRS = (
    os.path.join(ROOT_PATH, 'templates')
)

とあるように、ディフォルトではapp.yamlがあるディレクトリにtemplatesディレクトリを作ってそれ以下にテンプレートを置く。templates以下のディレクトリ構成は通常のDjangoと同じ。このサンプルの場合、templates以下にbase.htmlを置いて、さらにpollsディレクトリを作ってindex.html, detail.html, results.htmlを置いた。

  • index.html
{% extends "base.html" %}

{% block title %}Questions - My First Django App{% endblock %}

{% block content %}
<h3>Questions</h3>
{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li>
        {{ poll.question }}
        <a href="{% url polls.views.detail poll_id=poll.key.id %}">Vote</a> or 
        <a href="{% url polls.views.results poll_id=poll.key.id %}">Results</a>
        </li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
{% endblock %}
  • detail.html
{% extends "base.html" %}

{% block title %}Detail = My First Django App{% endblock %}

{% block content %}
<h3>{{ poll.question }}</h3>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="/polls/{{ poll.key.id }}/vote/" method="post">
{% for choice in choices %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.key.id }}"/>
    <label>{{ choice.choice }}</label><br />
{% endfor %}
    <input type="submit" value="Vote"/>
</form>
<br />
<a href="{% url polls.views.index %}">Back to Questions</a>
{% endblock %}
  • results.html
{% extends "base.html" %}

{% block title %}Results - My First Django App{% endblock %}

{% block content %}
<h3>{{ poll.question }}</h3>

<ul>
{% for choice in choices %}
    <li>{{ choice.choice }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<br />
<a href="{% url polls.views.index %}">Back to Questions</a>
{% endblock %}

(base.htmlにはGAEだからの違いは無いので省略)


14. アプリケーションをupdate
マニュアルどおり。mysiteの(app.yamlがあるところよりも)一つ上のディレクトリで実行

appcfg.py update mysite

このとき、app.yamlのapplication名がhttp://appengine.google.com/にログインして作ったアプリケーション名と一致していることを確認。app.yamlのversion番号はAdministrationのVersionsで使える。app.yamlに書くのはmajorバージョン。minorバージョンはupdateするたびに自動的に更新される。


、、、ということで出来上がったのが、http://servletgarden.appspot.com/polls/です。


最終的なディレクトリ構成はこんなふうに:

mysite -+- .google-appengine -> /usr/local/google-appengine
        +- __init__.py
        +- app.yaml
        +- appengine-django -+- __init__py
        |                    +- auth
        |                    +- ...
        |                    +- ...
        +- main.py
        +- manage.py
        +- polls.py -+- __init__.py
        |            +- models.py
        |            +- viwes.py
        +- settings.py
        +- stylesheets -+- main.css
        +- templates -+- base.html
        |             +- polls -+- detail.html
        |                       +- index.html
        |                       +- results.html
        +- urls.py

(他に、自動生成されるindex.yaml, *.pycがある)


感想:
Datastore関係がまるで違うので、難しかった。(Pythonの知識もDjangoの知識も無いし (^^;; )
Datastoreの中身はPCを再起動したらすべて消えていました。-Tmp-ディレクトリですからねぇ。中身をとっておいてimportする方法をどこかで見たような記憶が。。。いずれにせよ、datastoreまわりのツールは貧弱。phpMyAdmin(http://www.phpmyadmin.net/home_page/index.php)のような便利なツールが欲しいと思う。
administrationツールでentityを登録するときに日本語でも問題なく使えるのはうれしいかも。素のDjangoのadministrationツールは何かするとできるはずだとは思うのですが、少なくても何かをしないと日本語を入力できなさそうなので。
(わかりやすい)ドキュメントやら情報やらが不足しているような気がする。かなーり、googlingしているつもりなのに、必要なところにたどりつけなかったり。