Google App EngineとDjango
ずいぶん前のことになってしまいましたが、申し込みしてしばらく待った後、Google App Engine(GAE)のアカウントをいただいていました。せっかくアカウントを取得したので何か試してみようと思い、Djangoのチュートリアルにあるサンプルを動かしてみました。が、ことのほか苦労したので忘れないようにメモです。
PythonもDjangoもGAEも今回初めて使ったので、苦労するのは仕方が無いのですが、GAE特有のところはまだ情報があまり多くなくて特に苦労しました。とりわけDatastore関係です。 MySQLやPostgreSQLのようなリレーショナルデータベースではなく、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しているつもりなのに、必要なところにたどりつけなかったり。