Djangoチュートリアルを適当に進める~その2~

2018年12月17日

公式チュートリアルの続き。
今回対応するページはここ。
https://docs.djangoproject.com/en/2.1/intro/tutorial02/
データベースをセットアプして最初のmodelファイルを作り、Djangoのご機嫌な機能である管理画面を使う。
これ、Flaskとかでは外部ライブラリをインポートしないと管理画面とかないのでほんとありがたい。

データベースを作る!

mysite/settings.pyを開く。
標準設定ではSQLiteという種類のデータベースを作るようになっている。
まあ、実際に運用するときはmysqlとかPostgreSQLとか使うだろうけど、今回はお試しと言うことでSQLiteで行こうや。他のDBを使う方法はまた違うときに。
TIME_ZONEを東京に設定する。これ重要。ここんとこ飛ばすと後のアプリテストのとこで泣く。

#TIME_ZONE = 'UTC'

TIME_ZONE = 'Asia/Tokyo'

次はデータベースの準備。

$ python manage.py migrate

このコマンドを打つと、先ほどのsetting.pyからINSTALLED_APPSとデータベース設定の部分を読み込んでデータベースの作成が行われる。

modelを作る

modelとは、データベースの構造やふるまいを決定する記述のこと。データベースの構造はスキーマとも言われる。
今回つくるアプリではQuestion(問い)とChoice(選択)の二つを作る。
Choiceは二つの項目(フィールド、Field)を持っている。一つ目は選択肢の文、二つ目はその選択肢がどれだけ投票数を得たか。そして、それぞれの選択肢はいくつかあるQuestionのどれかに属している。
スプラトゥーンのフェスをイメージすると良い。
例えば2019年一発目のフェスのお題は「年末年始は誰と過ごす? 家族 vs 仲間」だった。
Questionは「年末年始は誰と過ごす?」でChoiceは二つフィールドをもっていて、「家族」か「仲間」といった選択肢と、それぞれ何人が投票したか。みたいな。
このデータベース構造を表すためにはPythonのclassを使う。
pollsフォルダのmodels.pyを次のようにする。

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Question、そしてChoiceのそれぞれのmodelがどういう情報を入れられるかは、CharFieldやDateTimeFieldといったField classで表されている。
CharFieldは文字を入れる場所であることを示し、DateTimeFieldは日時を入れる。
そして、それぞれの情報を入れる場所にはquestion_textやchoice_textといった名称をつけた。今後プログラム内ではこの名称を使ってデータを格納したり、呼び出したりする。
今回設定したCharFieldではmax_lengthという項目を設定することが必須だ。これは、データ内に格納できる文字数を設定する項目である。これはデータを格納するときだけでなく、バリデーションにも使われる。バリデーションとは、ウェブ上からデータを入力したときに、様式にそっているかチェックすること。
他にも、項目(Field)には初期値の設定が出来たりする。votesのdefault=0は初期値が0であることを表している。
最後に、ForeignKeyってところは、Choiceがそれぞれ一つのQuestionに対応していることを示している。

model達を有効化セヨ

modelファイルの中身を作ったことで、Djangoから色々出来るようになった。
しかし、データベースにmodelファイルの内容を適用するにはもうワンステップ必要だ。
それは、Djangoにpollsアプリを認識させること。
そのためにはmysite/settings.pyで、INSTALLED_APPSに’polls.apps.PollsConfig’を書き足す必要がある。
これはpollsフォルダのapps.pyにあるclass PollsConfigのことだ。

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

さて、それでは改めてコマンドラインに戻って、以下のコマンドを実行する。

$ python manage.py makemigrations polls

これはpollsアプリの「migrate」を実行しているわけではない。
「migrate」に必要なファイルを作成しただけ。
modelの中身を変更して「migrate」を実行するのではダメみたい。
このコマンドを実行すると「migrate」の準備用ファイルが出来る。
polls/migrations/0001_initial.pyがそれ。
どんな内容かはコマンドラインから下記のコマンドを打つことで分かる。

$ python manage.py sqlmigrate polls 0001

そうするとこんな画面が表示される。

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL);
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" RENAME TO "polls_choice__old";
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "question_id" integer NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED);
INSERT INTO "polls_choice" ("id", "choice_text", "votes", "question_id") SELECT "id", "choice_text", "votes", NULL FROM "polls_choice__old";
DROP TABLE "polls_choice__old";
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
COMMIT;

このコマンドによる出力は使っているmysqlやPostgreSQL等、データベースによって異なる。
テーブルの名前はアプリとモデルの名前を組み合わせたものになる。
PRIMARY KEYと言うFieldは勝手に作られる。
modelでForeign keyとしたFieldは勝手に○○_id(○○は紐づく先)というFieldの名前になる。

さて、この内容をデータベースに適用するために「migrate」を実行する。

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Applying polls.0001_initial... OK

さっき出来たpolls.0001_initialが適用されているのがわかる。
この「migrate」コマンドがあることによって、modelを変更したとき、つまりデータベースの構造を修正したいときに、わざわざ今あるデータベースを削除して作り直さなくて良いし、データベースの中にデータが格納されていた場合は、それを失わずにデータベースの構造を変えることができる。
Djangoで言うところの「migrate」は、一般的に日本語でマイグレーションと言われる。この作業はデータベースを新たな構造に移行する作業と言える。なんかくどくなってしまったが、他のフレームワークでも良く使われる用語なので覚えていた方がいい。
Djangoにおける一連のマイグレーション作業をまとめるとこんな感じ。

①models.pyの内容を更新
②python manage.py makemigrationsを実行して下準備。段取り八分。
③python manage.py migrateを実行してデータベースを更新。

シェルで遊ぶ

シェルとは対話式にプログラムを実行できるサービスである。
以下のコマンドをコマンドラインに打ち込む。

$ python manage.py shell

普通、Pythonのシェルを起動するときはpython shellだけでOKだが、ここではDjangoの設定を読み込んで実行するためにmanage.pyを間に噛ませている。

# modelをimportする
In [1]: from polls.models import Choice, Question
# Questionのデータを全て表示
In [2]: Question.objects.all()
Out[2]: <QuerySet []>
# 時間を取得するライブラリをimport。settings.pyに設定されたタイムゾーンが使用される
In [3]: from django.utils import timezone
# Questionのデータのインスタンスをつくる。中身にテキスト「何が新しい?」と作成日時を入れる。
In [4]: q = Question(question_text="What's new?", pub_date= timezone.now())

# データベースにセーブ
In [5]: q.save()

# 自動的にidが割り振られる。確認。
In [6]: q.id
Out[6]: 1
# 入れたテキストを確認。
In [7]: q.question_text
Out[7]: "What's new?"
# 作成日時を確認
In [8]: q.pub_date
Out[8]: datetime.datetime(2018, 12, 16, 0, 35, 17, 196086, tzinfo=<UTC>)
# テキストを変えてみる。
In [9]: q.question_text = "What's up?"
# セーブ。
In [10]: q.save()
# Questionのデータを全て表示
In [11]: Question.objects.all()
Out[11]: <QuerySet [<Question: Question object (1)>]>
# 一旦シェルを終了する
In [12]: exit

<Question: Question object (1)>ってなんて不親切なんだ。
というわけで、データの中身が表示されるようにpolls/models.pyを修正してみよう。
__str__()メソッドを使う。

class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text

class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

ちなみに、この__str__()メソッドはシェルから参照するときにデータの中身が表示されて便利・・・・というだけでなく、あとで触れる管理画面でも生きてくる。
__str()__はPythonの標準メソッドだけど、これからオリジナルのメソッドを追加してみる。

# 日時の計算を行うのでdatetimeライブラリをimport
import datetime
from django.db import models
# さっきシェルの時に使ったtimezoneライブラリをimport
from django.utils import timezone

class Question(models.Model):
    # ...
	# これがオリジナルのメソッド。pub_dateが1日以内であればtrueを返す。そうじゃなければfalse
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

では改めてpython manage.py shellを実行して、さっき追加したメソッドを確認してみる。

# さっきと同じようにmodelsからChoiceとQuestionをimport
In [1]: from polls.models import Choice, Question
# Questionに入っているデータを全部表示
In [2]: Question.objects.all()
Out[2]: <QuerySet [<Question: What's up?>]>
# id=1のデータだけを表示するためにfilterを使う
In [3]: Question.objects.filter(id=1)
Out[3]: <QuerySet [<Question: What's up?>]>
# 今度は「what」から始まるデータだけを表示するためにfilterとstartswithを使う
In [4]: Question.objects.filter(question_text__startswith='w
   ...: hat'))
  File "<ipython-input-4-6af8338701d5>", line 1
    Question.objects.filter(question_text__startswith='what'))
                                                             ^
SyntaxError: invalid syntax
# ↑閉じるカッコを二つ付ける痛恨のミス(良くやる

In [5]: Question.objects.filter(question_text__startswith='what')
Out[5]: <QuerySet [<Question: What's up?>]>

# timezoneメソッドを読み込み
In [6]: from django.utils import timezone
# 今年の年数を変数に入れる
In [7]: current_year = timezone.now().year
# pub_dateが今年のデータだけを表示
In [8]: Question.objects.get(pub_date__year=current_year)
Out[8]: 
# id=2のデータだけを表示。もちろん無い。
In [9]: Question.objects.get(id=2)
------------------------------------------------------------
DoesNotExist               Traceback (most recent call last)
<ipython-input-9-75091ca84516> in <module>()
----> 1 Question.objects.get(id=2)

.....中略

DoesNotExist: Question matching query does not exist.

# PRIMARY KEYで検索するにはpkを使う
In [10]: Question.objects.get(pk=1)
Out[10]: <Question: What's up?>
# 取り出したデータを変数に代入
In [11]: q = Question.objects.get(pk=1)
# 先ほど作ったメソッドを実行してみる
In [12]: q.was_published_recently()
# これ、一日以上たった後にやったので「False」が出てしまった。このチュートリアルを続けてやっていれば「True」になるハズ。
Out[12]: False
# 先ほど取得したデータに紐づいているChoiceのデータを表示する。setを使う。
In [13]: q.choice_set.all()
# なんもない。
Out[13]: <QuerySet []>
# 追加しよう。引き続きsetを使う。votesの初期値は何も入れなくてもゼロが入るハズ・・・。
In [14]: q.choice_set.create(choice_text='Not much', votes=0
    ...: )
Out[14]: <Choice: Not much>
# もういっちょ。
In [15]: q.choice_set.create(choice_text='The sky', votes=0)
    ...: 
Out[15]: <Choice: The sky>
# あそーれ。ここでは変数の中にChoiceのデータを入れてみる。
In [16]: c = q.choice_set.create(choice_text='Just hacking again', votes=0)
# Choiceのデータに紐づけられたQuestionを表示。
In [17]: c.question
Out[17]: <Question: What's up?>
# 先ほどの変数、qに戻って紐づけられたChoiceデータを全部表示
In [18]: q.choice_set.all()
Out[18]: <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
# 何個あるか確認
In [19]: q.choice_set.count()
Out[19]: 3
# 今年に作られたかどうかでフィルターをかける
In [20]: Choice.objects.filter(question__pub_date__year=curr
    ...: ent_year)
Out[20]: <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
# 変数の中に「Just hacking」から始まるChoiceのデータを入れる
In [21]: c = q.choice_set.filter(choice_text__startswith='Just hacking')
# 削除する
In [22]: c.delete()
Out[22]: (1, {'polls.Choice': 1})
# おしまい。尚、いろいろChoiceでデータを作ったけど、saveを使ってないから、保存はされていない。
In [23]: exit

上のシェルの中で何回かアンダースコアが二つ並ぶものが出来て来たけど、あれはAPIを通してフィールドを見ている。

管理画面への招待状

管理ユーザーを作ろうず

$ python manage.py createsuperuser

すると以下の画面が出てユーザー名やEmailやパスワードを聞かれるから入力していく。

Username (leave blank to use 'XXXXXX'):
Email address: 
Password: 
Password (again): 
Superuser created successfully.

さて、python manage.py runserverを打ち込んでサーバーを起動して管理画面を確認しよう。
管理画面のアドレスは以下の通り。
http://127.0.0.1:8000/admin/

するとこんな画面があなたを出迎える。

さっき設定したユーザー名とパスワードを入力してログインしよう。
するとこんなページが出る。

これが管理画面。
データベースの内容をいじれる。
これはユーザーの管理を行う画面みたい。
んがしかし、さっき色々といじっていたQuestionがない。
これはどういうことか。
というわけで、管理画面からQuestionをいじれるようにする。
一旦、サーバーを停止して、polls/admin.pyを開いて以下の行を追加しよう。

from django.contrib import admin

from .models import Question

admin.site.register(Question)

サーバーを再開してみよう。Questionが表示されているハズ。

Questionをクリックすると以下の画面になる。ここから、what’s up(調子どう?)をクリックすると編集画面に移動する。

次に出てくる編集画面はmodelから自動作成されたもの。
この画面でTodayとNowをそれぞれクリックしてみよう。

んでもって、「Save and continue editing」をクリック。
こうなる。

ページの右上、「Histry」をクリックすると、今行った日時変更の操作の履歴を見ることができる。

次に続く。

Djangoチュートリアルを適当に進める~その3~