はじめに
前回、DockerでDjango環境を作り、簡単に触ってみたので今度は前回の補足を兼ねつつ公式チュートリアルを通してDjangoを少し掘り下げていきます。
今回はチュートリアルの1~3から個人的に大切だなと感じたところをPUしています。
include()について。
include()はURLconf(ここではurl.py)への参照を表す関数です。
引数にurls.pyがあるパス、第2引数に参照するurl.pyの名前を設定する。
ここではpollsのurls.pyを参照したいので、polls.urlsを設定することになります。
path()の引数はroute、view、kwargs、nameの4つを設定できます。
routeはそのアプリケーションのルートアドレスになります。
viewは呼び出したいview関数を指定します。
kwargsは任意のキーワードを辞書としてviewに渡すことができます、省略可能です。
nameはURLに名前付けがある場合、それを指定するとDjangoのどこからでも参照できるグローバルなURLになります。
以上2つの例を見てみます。
path('polls/', include('polls.urls'))
例えばこの記述だとrouteは'polls/'の部分になり、viewはinclude('polls.urls')となります。
include('polls.urls')にviewなんてあったっけ? と思ったらもう一度urls.pyを見てみましょう。冒頭にインポートされてるのがわかると思います。
ちなみに第2引数に入るviewは今回のようなinclude()の他にas_viewsの出力も取ることができます。
as_viewsはDjangoにおけるビューの条件を満たす関数を作るclassonlymethodです。
詳しいことは 参考記事やドキュメントを見るとして、噛み砕くとまずDjangoにはdispatchメソッドというリクエストメソッドを判定してそのリクエストメソッドと同じ名前のメソッドを実行するメソッドがあります。
今回の場合は例えば前回の記事の
class SampleView(View):
def get(self, request, *args, **kwargs):
return render(request, 'app_folder/page01.html')
def post(self, request, *args, **kwargs):
input_data = request.POST['input_data']
result = SampleDB.objects.filter(sample1=input_data)
result_sample1 = result[0].sample1
result_sample2 = result[0].sample2
context={'result_sample1':result_sample1, 'result_sample2':result_sample2}
return render(request, 'app_folder/page02.html', context=context,)
top_page = SampleView.as_view()
この部分においてですが、ここでrequest.method == 'GET'だった場合はSampleView.get()が実行されるということになります。
as_viewはclsとしてインスタンス化して(クラスメソッドのインスタンスになるときクラス自身はselfではなくclsで表す)、このdispatchメソッドを実行するような関数を生成し、それをreturnするというものになり、それはすなわちviewに分類されるから引数として取ることができるということになります。
さてnameとは何かというと
urlpatterns = [
path('', views.index, name='index')
]
こういうやつですね。
こうするとルートインデックス/indexあるいはルートインデックス/の形でURLを叩いたときにviews.indexが呼ばれるという処理にすることができます。
最後の方にもっとわかりやすい使い方が出てきますのでとりあえずここではパスに名前がつくということだけ覚えておきます。
Djangoでのリレーション
from django.db import models
# Create your models here.
class Question(models.Model):
question_next = 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)
models.Foreignkey(リレーションさせるテーブルのクラス名, on_delete=models.CASCADE)の形で設定します。
この場合はQuestion:Choice = 1:多 という関係になります。
わかりやすくいうと1つの質問に対して答えは複数あるという関係ですね。
on_delete以下は参照するオブジェクトが削除されたときにそれと紐付けられたオブジェクトも一緒に削除するのか、残しておくのかというのを設定しています。
CASCADEの場合は紐付けられたオブジェクトをすべて削除するということになります。
例えば
# polls_question
id question_next
0 What`s_up?
# polls_choice
id choice_text votes question_id
0 I`m_heading 0 0
1 Stomachache 1 1
とみたいなテーブルがあるとします。
ここで、polls_questionテーブルのid=0のデータを削除します、するとそれを参照しているpolls_choiceテーブルのid=0及びid=1のデータも削除されるということになります。
これがCASCADEを指定した場合のデータ削除処理になります。
他にもオプションはあるのですが、CASCADEを使うことが多いようです。
リレーション関係にあるデータは扱いが少し難しいので削除する際は依存関係にある双方のテーブルのデータは一方を消すときは全部消すのがベターという解釈を私はしています。
もちろん他方をnull扱いにしたり、他方だけ完全に削除するという指定をもできます。
Djangoでのマイグレーション及びAPI操作(データベースとのやりとり)
さて、ここまで終わればあとはmigrateでマイグレーションを行えばテーブルの完成ですが、SQL文を出力してmigrateが何をしているか示してくれるコマンドがあるので試してみます。
python manage.py makemigrations polls
dockerの場合は適宜docker execコマンドでコンテナの中に入りmanage.pyファイルがあるディレクトリに移動して実行してください。
makemigrationsコマンドにアプリケーションフォルダの名前を指定すると、そのアプリケーションのマイグレーションを保存することができます。
例えば今回このコマンドを実行すると
Migrations for 'polls':
polls/migrations/0001_initial.py
- Create model Question
- Create model Choice
このように0001という名前がマイグレーションに与えられてファイルが保存されます。
ちなみに今回私は間違えてマイグレーションしてしまい、テーブルに登録するデータのプロパティを間違えてしまったので変更して再度makemigrationsをしました。
その場合は以下のような0002_auto_20200408_1848.pyというファイルが作られ再度migrateするとテーブルが更新されます。
# Generated by Django 3.0 on 2020-04-08 18:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('polls', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='question',
old_name='question_next',
new_name='question_text',
),
]
次にこのマイグレーションをmigrateするとどのような処理が行われるかを見てみます。
py manage.py sqlmigrate polls 0001
このようにsqlmigrateコマンドにアプリケーションフォルダとマイグレーションを指定すると
--
-- Create model Question
--
CREATE TABLE `polls_question` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `question_next` varchar(200) NOT NULL, `pub_date` datetime(6) NOT NULL);
--
-- Create model Choice
--
CREATE TABLE `polls_choice` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `choice_text` varchar(200) NOT NULL, `votes` integer NOT NULL, `question_id` integer NOT NULL);
ALTER TABLE `polls_choice` ADD CONSTRAINT `polls_choice_question_id_c5b4b260_fk_polls_question_id` FOREIGN KEY (`question_id`) REFERENCES `polls_question` (`id`);
このようにSQL文が表示され、migrateコマンドが何をやっているかわかるようになります。
どのようなSQL文でテーブルを作るのか一目瞭然ですね。
ここまで確認したら、migrateしてテーブルを作りましょう。
ではチュートリアル通りshellコマンドを試してみて、データベースとやりとりをしてみます。
# モデルをインポートしてクラスを取得する
>>> from polls.models import Choice, Question # Import the model classes we just wrote.
# Questionクラスのオブジェクトをすべて取得する。データが入っていないので結果は空で返ってくる。
>>> Question.objects.all()
OUT:<QuerySet []>
# datetimeモジュールを使うために、django.utilsからその中のtimezoneクラスをインポートする。
>>> from django.utils import timezone
# 変数にオブジェクトの形でデータを代入
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
# データをセーブ
>>> q.save()
# データのプロパティ取得
>>> q.id
1
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)
# プロパティの変更をこころみる
>>> q.question_text = "What's up?"
>>> q.save()
# 失敗
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>
失敗を回避するためにmodels.pyを書き換えます。
import datetime
from django.db import models
from django.utils import timezone
# Create your models here.
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
# 追加1
def __str__(self):
return self.question_text
# 追加2
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
# 追加3
def __str__(self):
return self.choice_text
__str__()はオブジェクトメソッドです。
例えばここでは引数にselfを設定して、クラスをインスタス化して各プロパティを人間可読にするという役割を担っています。
先程のQuestion.objects.all()でオブジェクトがうまく表示されなかったのはオブジェクトが機械可読のままであったからということになります。
つまり、オブジェクトはそのままでは私達の目にはこのクラスのオブジェクトですという見え方でしかないので、そこに名前をつけてやるということをやるわけです。
追加2はオリジナルメソッドになります。
returnのあとに数式なのでTrue or Falseを返すということになります。
timezone.now()が現在の日付、datetime.timedelta(days=1)が1日後ということになるので今日 - 明日は昨日、それ以上でself.pub_dateは真になる。
つまり、Questionテーブルのデータが昨日以降に作成された場合にTrueを返すメソッドになります。
しかし、このままではいわゆる未来日付と呼ばれる値であった場合にもTrueが返ってきてしまうバグを抱えています。
Questionテーブルということは質問の内容を登録するということになりますが、質問をしたということは時間的には必ず過去または現在でなければなりません。
昨日質問した、今日質問したとはいいますが、明日質問したとは言いませんよね?
このバグについてはチュートリアルの4以降の項目で解消をするようなのでとりあえずこのままおいておきます。
さて、再度shellコマンドを実行します。
>>> from polls.models import Choice, Question
# __str__メソッドを追記したのでQuestionプロパティのquestion_textがオブジェクトの名前として呼び出される。
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
# filterメソッドでidでオブジェクトを絞り込めることを確認。
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
# 同じようにquestion_textがwhatで始まるオブジェクトを絞り込んでみる。
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>
# 今年追加されたオブジェクトを取得する
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>
# id=2のデータは存在しないのでエラー
>>> Question.objects.get(id=2)
Traceback (most recent call last):
...
DoesNotExist: Question matching query does not exist.
# 主キーが1のオブジェクトを取得。通常主キーはid。なのでここではpk=1 == id=1となる
>>> Question.objects.get(pk=1)
<Question: What's up?>
# 追加2のメソッドの動作確認
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
# 主キーが1であるオブジェクトをインスタンス化。
>>> q = Question.objects.get(pk=1)
# 以上のオブジェクトに紐付くchoiceオブジェクトを呼び出す。要は質問に対して答えがいくつあるかということになる。Choiceテーブルにはデータがないので空で返ってくる。
>>> q.choice_set.all()
<QuerySet []>
# Question.objects.get(pk=1)に紐付く答えとして、choiceテーブルにデータを3つ追加する。
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
# questionプロパティはChoiceテーブルの外部キー、つまりQuestionテーブルの主キー参照となる。
>>> c.question
# よって上述の通りc変数はq = Question.objects.get(pk=1)に紐付いているので下記の結果が返ってくる。
<Question: What's up?>
# q = Question.objects.get(pk=1)に紐ついているChoiceオブジェクトを呼び出して、オブジェクトの数をカウント
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3
# Choiceオブジェクトのうち、参照しているQuestionテーブルの主キーに紐付く質問の作成日が今年であるオブジェクトを絞り込む。
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
# 特定のChoiceオブジェクトを削除する。
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()
views.pyを通してURLconfの役割をもう少し見てみる。
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from .models import Question
# Create your views here.
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {
'latest_question_list': latest_question_list,
}
return render(request, 'polls/index.html', context)
def detail(request, question_id):
"""
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detaill.html', {'question': question})
"""
# 上記はDjangoのショートカットだとこうなる。get_object_or_404メソッド
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
def results(request, question_id):
response = "You`re looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You`re voting on question %s." question_id)
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('<int:question_id>/results/', views.results, name='results'),
path('<int:question_id>/vote/', vies.vote, name='vote'),
]
path()については先程言及しました。
例えばここで/polls/34/へのアクセスがあったとします。
そうすると上記のような設定だとどういう処理になるか覚えていますでしょうか?
正解はurls.pyのurlpatternsを参照して、どのview関数にリクエストを飛ばすかという判断がなされ、そこで呼び出されたview関数を実行するということになります。
もう少し掘り下げてみます。
まず、path()の第1引数に指定された部分を見ます。この部分にはルートアドレスが入るというのは先程やりましたね。
例えば1番目の記述がわかりやすいです。この書き方だとここで指定されるURLパターンは/polls/ということになります。
つまり、第1引数の指定が空でもpolls/は自動的に指定されているというのがわかりますね。(指定なしはエラーになります。)
これはDjangoはリクエストを受け取るとまず、settings.pyのROOT_URLCONFに指定されているurls.pyファイルをロードします。
これはシステム全体のURL設計をするファイルが指定されています。前回の例でいうとproject/project/urls.pyがそれにあたります。
ではそこにこんな記述があるとしましょう。
urlpatterns = [
# 管理サイトにアクセスするURL
path('admin/', admin.site.urls),
# 今回作成するアプリ「app」にアクセスするURL
path('app/', include('app.urls')),
# 何もURLを指定しない場合
path('', views.index, name='index'),
path('polls/', include('polls.urls')),
]
ここまで来ればもうからくりがわかったという人もいるでしょう。
最初のpath()の部分は呪文だと思ってください。3番目の記述は先程と同様のものです。
注目するべきは2・4番目の記述です。
ここでこういう記述をすることで、各アプリのurls.pyが参照できるようになり、さらにそのurlpatternsという変数とリクエストをマッチングさせられるということになります。
また、ここで path('polls/', include('polls.urls'))と記述することでpolls/url.pyでは/polls/34/といったようなリクエストがあった場合、すでにpollsはマッチしてると判断され、それを除いた部分がどのpath()とマッチするかの判断がなされます。
そうなると/polls/34/はpolls/url.pyの1番目のpath()とはマッチングしないことになります。
では2番目以降の記述だとどうなるかというと、<int:question_id>を見てみます。
これはURLパターンからintを探してそれをquestion_idという名前にするという意味になります。
ドキュメントより
山括弧を使用すると、URLの一部が「キャプチャ」され、キーワード引数としてビュー関数に送信します。 文字列の :question_id> 部分は、一致するパターンを識別するために使用される名前を定義し、<int: 部分は、URLパスのこの部分に一致するパターンを決定するコンバータです。
こうなると/polls/34/というアクセスは34/という文字列が残ることになるので2番目のpath()とマッチングするということになります。
そうなると後は指定されたview関数にリクエストが渡されます。
この場合はpolls/views.pyのdetail()メソッドにリクエストがパスされます。
見てみると、引数にrequest及びquestion_idが指定されているので、結果として
detail(request=<HttpRequest object>, question_id=34)
といったようにdetail()に引数が渡されて、メソッドが実行されていくということになります。
実際にどのような処理がなされているかは後ほど確認します。
templatesについて
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
settings.pyのこの部分が肝で、まずDIRSでどこにテンプレートhtmlを置くかを指定します。
1つのアプリケーションのだけのテンプレートの指定ならBASE_DIRではなくアプリケーションフォルダ名が入りますが、BASE_DIRとしたあとAPP_DIRSをTrueにすることで上で指定したフォルダのサブディレクトリをアプリーケーションフォルダの名前で検索してテンプレートを適用するような設定にすることができます。
つまり、今回ならアプリケーションフォルダ名はpollsなので、そのテンプレートを保存するディレクトリはtemplates/pollsということになります。
render()メソッドは第1引数にrequest、第2引数にテンプレート名、第3引数に辞書を指定できます。
そうして、受け取った引数を元にHttpResponseオブジェクトを作成してそれを返すという役割があります。
こういう処理のことをレンダリングと呼ぶそうです。
レンダリングはある情報を形にを変えて表現するような処理というイメージで、今回の場合はrequestオブジェクト(sessionやget,post)・context(データベースのデータ)などの情報とテンプレートのhtmlファイルとをうまく組み合わせ、
ブラウザの画面として成形するような処理をしているみたいです。
またrenderクラスをインポートすることで、loaderクラスやHttpResponseクラスをインポートしなくて済むようになります。
ただし、HttpResponseクラスは他の役割もあるので残しておいた方が無難みたいです。
次に例外です。
from django.http import Http404
from django.shortcuts import render
from .models import Question
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})
いわゆるtry-catchのよく見る形です。
django.httpモジュールからHttp404クラスをインポートすることで404エラーを出すことができます。
question変数にQuestionオブジェクトを代入したのものが存在するか否かで例外を出すというコードです。
もう少しわかりやすくいうと、リクエストされたIDを持つデータが存在しない場合は例外を出すという処理になります。
これもloader、HttpResposeの役割を担ったrender()メソッドと同じくdjango.shortcutsモジュールからより簡潔に書けるメソッドをインポートできます。
from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
get_object_or_404()メソッドにモデル名とキーを指定することで自動的にtry-catchの処理を行ってくれます。
キーは複数設定することができます、例えばget_object_or_404(User, pk=1, name='tanaka')のような指定の場合主キーが1、nameキーの値がtanakaのデータを取得するか404エラーを出すということになります。
ちなみにget_list_or_404()というメソッドもあります。
同じようにモデルから値を取得するメソッドですが、今度は指定したキーを元にfilter()メソッドで絞り込んでlistですべて取得するという処理をします。
例えば先程のようにget_list_or_404(User, pk=1, name='tanaka')と指定すると主キーが1、nameキーの値がtanakaのデータをすべて取得しリストにして返すか1つも存在しない場合は404エラーを出すということになります。
テンプレートシステムについて
Laravelでもあったテンプレートシステムですが書き方や用法にあまり違いはないです。
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
例えばこういう記述ですが、おそらく見ただけで気づいた方もいらっしゃると思いますが{{}}と波括弧2重だと変数の表現になります。
もっというと、変数の属性を表現しているということになり、ここにはQuestionオブジェクトのquestion_textの部分が入りますよということが書かれているわけです。
処理としては
ドキュメントより
テンプレートシステムは、変数の属性にアクセスするためにドット検索の構文を使用します。 {{ question.question_text }} を例にすると、はじめに Django は question オブジェクトに辞書検索を行います。それに失敗したら、今度は属性として検索を行い、このケースの場合は成功します。もし属性の検索に失敗すると、リストインデックスでの検索を行います。
ということになります。
今回のチュートリアルで作成していることでいうとh1タグの中には質問の内容が入るということがわかると思います。
よって質問が複数あり、その都度テンプレートを作っていては大変……ということでこういった書き方をして、質問の内容によってh1タグに入る内容が変わりますよということがわかればOKです。
次に{}という波括弧1つの場合。これは、メソッドの呼び出しを行っています。
今回の処理は以下の通りです。
ドキュメントより
メソッドの呼び出しは {% for %} ループの中で行われています。 question.choice_set.all は、 Python コードの question.choice_set.all() と解釈されます。その結果、Choice オブジェクトからなるイテレーション可能オブジェクトを返し、 {% for %} タグで使えるようになります。
つまり、このメソッドで得られたchoice.choice_textオブジェクトの数だけliタグによるリストができるということになります。
今回の場合は、質問の内容に対しての答えの表現ということになります。
1つの質問に対して、答え方は複数あるのでこういう形で表現しているということがわかればOKです。
また例えば、polls.urlsにおいて先程nameを指定したことを覚えていますでしょうか?
path('<int:question_id>/', views.detail, name='detail'),
こういうのですね。
このように指定しておくと、テンプレートにおいてurlタグを使うことでURLパスを書く必要がなくなります。
例えば
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
こういう記述です。
普通ならaタグのhref属性にはURLやパスを指定しなければなりません。
しかし、前もってdetailは/polls/'<int:question_id>/'というパスの名前ですよということを定義してあります。
ということで、以上のようにdetailと書いてあとはquestion_idという名前のint型をおけば(ここではquestion.id)パスになるという寸法です。
余談ですが例えばこのパスをpolls/specifics/12/のような形にしたいならpolls/urls.pyを編集しましょう。
名前空間
最後に名前空間です。
各アプリケーションフォルダのurls.pyに予め名前空間を定義することで1つのプロジェクト内で複数のアプリケーションフォルダを管理する際にどのアプリケーションのURLconfを使えばいいか自動的に判断してくれます。
# 以下を追加
app_name = 'polls'
このように指定します。
そしてテンプレート側でURLパスの名前指定の部分で
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
このように名前空間付きで指定してやります。
そうするとpollsアプリのdetailビューのパスがここに入りますという表現になるということになります。
参考
レンダリングってそもそも何?【初心者向けに分かりやすく解説】
Djangoのクラスベースビューのas_viewて何なの?