この記事の位置付け
- 対象: DjangoでなんとなくWebアプリケーションが作れるようになったくらいの人
- 目的: Djangoで気をつける部分を網羅して自分用の備忘とする
- 適宜ノウハウを追加して集約しておく
Starting up
プロジェクトとアプリの作成
- 設定系のファイルはconfigというアプリを作成してそこに内包するのが良い
django-admin startproject
コマンドを実施する時に.
をつけるとカレントディレクトリにアプリが構築される
$ mkdir `project名`
$ cd `project名`
$ django-admin startproject config .
ログ出力にロガーを使う
- ローカルで作っているときは
print
で事足りるが、サーバーにデプロイするとログはどこかのファイルに吐き出しておきたくなる settings.py
に下記を追加
LOGGING = {
'version': 1, # これを設定しないと怒られる
'formatters': { # 出力フォーマットを文字列形式で指定する
'all': { # 出力フォーマットに`all`という名前をつける
'format': '\t'.join([
"[%(levelname)s]",
"asctime:%(asctime)s",
"module:%(module)s",
"message:%(message)s",
"process:%(process)d",
"thread:%(thread)d",
])
},
},
'handlers': { # ログをどこに出すかの設定
'file': { # どこに出すかの設定に名前をつける `file`という名前をつけている
'level': 'DEBUG', # DEBUG以上のログを取り扱うという意味
'class': 'logging.FileHandler', # ログを出力するためのクラスを指定
'filename': os.path.join(BASE_DIR, 'django.log'), # どこに出すか
'formatter': 'all', # どの出力フォーマットで出すかを名前で指定
},
'console': { # どこに出すかの設定をもう一つ、こちらの設定には`console`という名前
'level': 'DEBUG',
# こちらは標準出力に出してくれるクラスを指定
'class': 'logging.StreamHandler',
'formatter': 'all'
},
},
'loggers': { # どんなloggerがあるかを設定する
'command': { # commandという名前のloggerを定義
'handlers': ['file', 'console'], # 先述のfile, consoleの設定で出力
'level': 'DEBUG',
},
},
}
- viewとかで下記のように記述すると上記で指定されたファイルにログが出力される
logger = logging.getLogger('command')
logger.info(msg)
Debug Toolbarを使う
- アプリケーションの速度が出ない時に、その原因を調査するためにも必須のライブラリ
- pipでインストール
$ pip install django-debug-toolbar
settings.py
に下記を追加
if DEBUG:
INTERNAL_IPS = ('127.0.0.1',)
MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware',
)
INSTALLED_APPS += (
'debug_toolbar',
)
# 表示する内容
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.versions.VersionsPanel',
'debug_toolbar.panels.timer.TimerPanel',
'debug_toolbar.panels.settings.SettingsPanel',
'debug_toolbar.panels.headers.HeadersPanel',
'debug_toolbar.panels.request.RequestPanel',
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.logging.LoggingPanel',
'debug_toolbar.panels.redirects.RedirectsPanel',
]
DEBUG_TOOLBAR_CONFIG = {
'INTERCEPT_REDIRECTS': False,
}
- urls.pyに下記を追加
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
Templatesをプロジェクト直下に移動する
- Templatesは
base.html
やWidgetなど、アプリケーション固有でないものも多い、のでアプリケーション内ではなくプロジェクト直下に置くのが個人的には吉 settings.py
を下記の通り変更
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # ← ここ
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Staticをプロジェクト直下に移動する
- TODO うまく動いていないのであとで調査
- cssやjs等のstaticファイルももTemplates同様にプロジェクト直下に置く場合は
settings.py
を下記のようにする
STATIC_URL = '/static/' # 配信用のディレクトリ名 Webサーバーははここに対して静的ファイルを探しに来る
STATICFILES_DIR = [os.path.join(BASE_DIR, 'staticc')] # staticファイル置き場を追加で指定したい時に利用。collectstaticがファイルを探す元に追加される
STATIC_ROOT = os.path.join(BASE_DIR, 'static_root') # python manage.py collectstatic コマンドが各staticファイルを集約する先
- Webサーバー → STATIC_URL
- Django staticファイル まとめ
- Djangoにおける静的ファイル(static file)の取り扱い
Model
Choiceの代わりにEnumを利用
import enum
# valueに文字を利用する場合はEnumを継承
class Category(enum.IntEnum):
Hoge = 1
Fuga = 2
@classmethod
def get_choices(cls):
return tuple((x.value, x.name) for x in cls)
class ModelUsingCategory(models.Model):
category = models.SmallIntegerField(choices=Category.get_choices())
- 名前や値へのアクセス
>>> Category(1).name
> 'Hoge'
>>> Category(1).value
> 1
インスタンス保存時/削除時の追加処理
- インスタンスの保存時や削除時に追加の処理を記述したい場合がある。その場合Modelクラスが持っている
save
delete
関数をオーバーライドする
class Hoge(models.Model):
fuga = models.CharField(max_length=128) # 適当なフィールド
...
def save(self, *args, **kwargs):
self.fuga += 'appended string' # 保存される前のインスタンスにアクセス可能
super(Hoge, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
# saveと同じで保存される前のインスタンスにアクセス可能
super(Hoge, self).delete(*args, **kwargs)
...
データ作成と最終更新のタイミングを管理する
- Modelに下記を追加すると、それでデータが作成された日時と最終更新された日時を保存する
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
ManyToMany(多対多): 通常の使い方
- 何かのModelファイルにManyToManyフィールドを追加するのみ
class Book(models.Model):
like = models.ManyToManyField(User) # 本に対してユーザーがイイねできる
...
- Model内にフィールドとして定義しているが、実際は中間テーブルが作成される
テーブルのカラムはid、Bookのプライマリーキー、そしてUserのプライマリーキー - ManyToManyのインスタンスにアクセスしたり、追加/削除する場合は下記のようにする
book = Book.objects.get(id=1)
user = User.objects.get(id=1)
book.like # アクセスするとquery_setが返る
user.book_set # 逆向きに参照する場合は `_set` を付ける
book.like.add(user) # 本に対してイイねしている人を追加
book.like.remove(user) # 削除
book.save()
ManyToMany(多対多): 追加の情報をもたせる
- 単に複数のデータと複数のデータが紐付いているということだけでなく追加情報を管理したい場合は、中間テーブル用のModelを明示的に作成
- 例えばイイねではなく、レビューを投稿できるようにすると、下記のような感じ
class Review(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# 0~5までの数値 validatorを定義しているがこの文脈では気にする必要なし
rating = models.FloatField(null=True, validators=[MaxValueValidator(5),MinValueValidator(0)])
comment = models.TextField(max_length=1024, null=True, blank=True) # コメントを受け付けられるようにする
Modelに関数を追加: インスタンスレベルはModel内で関数を宣言
- 複数回使うロジックがあるとModelの関数として集約して使い回せるようにすると良い場合がある
- 一つ一つのインスタンスレベルでの関数はModelのメンバとして宣言する
- 複数のインスタンスを取り出す時のロジックを共通化したい場合は後述のManagerの関数として宣言し、
objects
に紐付ける
Modelに関数を追加: クエリレベルはManagerの関数として宣言
- 例えば集計処理とか、
is_active
みたいなカラムがあってこれがTrue
のデータだけ取得したいみたいなことがあり、同じロジックを使いまわしたい時 - ModelにはManagerというものが最低1つは紐付いている
- Managerとは、データベースにクエリを投げてインスタンスを取得するみたいな部分を集約するためのもの
- なのでクエリレベルの処理はManagerを使うのが慣例
① Managerに関数を追加
- 例えば特定のキーワードがタイトルに含まれる本を取得するための関数を追加
class BookManager(models.Manager):
def title_count(self, keyword):
return self.filter(title__icontains=keyword).count()
class Book(models.Model):
...
objects = BookManager # 作成したManagerをobjectsとして紐づけ
...
- ちなみに
objects
でない名前にすることも可能 - 下記のようにするとこの関数を利用できる
>>> Book.objects.title_count('django')
> 4
② 元々のQuerySetを変更
- 通常のManagerクラス(objects)はDBの全てのデータを取得する
- これを制限するということが可能。例えば夏目漱石の本しか返さないManagerを定義すると下記のようになる
class DahlBookManager(models.Manager):
def get_queryset(self):
return super(DahlBookManager, self).get_queryset().filter(author='夏目漱石')
class Book(models.Model):
...
objects = models.Manager() # The default manager.
soseki_objects = DahlBookManager() # The soseki-specific manager.
...
View
View全体像
- Viewは用途に応じていろいろなものが用意されていて、それぞれの関連を概念的に理解したい
- 関数ベースビューとクラスベースビューのどっちを使うべきか
- 処理の複雑さによるが、基本的にはクラスベースビューを使うことが多い
- クラスベースビューにも複数ある。どれを使うべきか
- 用途に応じて使い分ける
- 一覧表示/詳細表示/データ作成/データ更新/データ削除
関数ベースビュー(Function Based View)
- FBVと略されることがある
- 前提として、
urls.py
ではURLに対して関数を紐づけておき、そのURLに対してリクエストがくると紐付いている関数が実行される - 関数なので、そのまま
urls.py
で利用する request
を受け取る-
例えば全ての本を取得する場合、下記のようになる
-
views.py
に下記のような関数を定義する
def books(request):
books = Book.objects.all()
context = {'books': books}
return render(request, 'cms/book_list.html', context)
urls.py
でこの関数を呼び出す
urlpatterns = [
url(r'books/$', views.books, name='books'),
]
- かなりシンプルだが、実際はページング処理とかも実装する必要がでてくる
- さらに、本の一覧ならGETのHTTPSリクエストさえさばければ良いが、例えば本の詳細ページでユーザーが本の情報を更新できるようにすると、POSTも受け付けられるようにしたい。その場合、下記のような関数になる
def book(request):
if request.method == 'POST':
# Code block for POST request
else:
# Code block for GET request
...
- このように、どんどん記述が増えてくる。そうするとクラスベースビューの方が楽に実装できるね、となる
- 参考リンク
- Python Django入門 (4) : 関数ベースビューのシンプルな実装の参考
- Class-Based Views vs. Function-Based Views : どっちを利用するべきか。Pros Cons等
クラスベースビュー(Class Based View)
- CBVと略されることもある
- クラスなので、
urls.py
では内包するas_view()
という関数を使う django.views.generic
に定義がある- クラスなので、様々な関数を保持している。例えば
def dispatch(self, request, *args, **kwargs):
はHTTPリクエストメソッド(GETやPOST等)に応じて、適切な関数に割り振るという処理を行う - クラスベースビューは用途によって複数種類用意されている。種類毎に、用途をを実現するために実装しないといけない関数や変数がある
- 参考リンク
- Djangoの 汎用クラスビューをまとめて、実装について言及する : どういったタイミングでどのクラスベースを使うかの知見
- Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル : 実装サンプル豊富
- Classy Class-Based Views. : 全クラスベースビューのリファレンス
django.generic.View
django.generic.View
は意外にWeb上にサンプルが少ないので載せておくinisial
をクラス変数として保つ必要がある
class HogeView(generic.View):
initial = {}
template_name = 'hogehoge.html'
def get(self, request, *args, **kwargs):
...
return render(request, self.template_name, context)
get
等の関数はgeneric.View
では定義されていないので、適宜書く必要があるurls.py
ではgeneric.View
に内包されたas_view()
という関数を利用する
urlpatterns = [
path('hoge/', views.HogeView.as_view(), name='hoge'),
]
- 例えばListViewであれば、何かの一覧表示をメインで行う、などコードを読んだ人に良くも悪くもバイアスをかけることになるので、一つのViewでCRUDのいろいろな処理をできるようにする場合などの特殊なViewは
generic.View
の利用を検討するのも良いかと
shortcuts(Viewの終着地点)
- Viewでは処理の結果、HTTPレスポンス等を送ったり他のViewへ飛ばす。いくつか種類があるので代表的なものをおさえておく
- 例えばListViewなどは
get
という関数をすでに持っており、その関数の中で呼び出しているdef get_queryset(self):
という関数をオーバーライドして処理を追加する。 こういった場合はレスポンスはあまり気にしない - 参考: Django Shortcuts
render()
- ポピュラー。templateにデータ(contect)を埋め込んでHTTPレスポンスとして返す関数
- 定義:
render(request, template_name, context=None, content_type=None, status=None, using=None)
- 関数ベースビューか
django.view.generic.View
を利用する場合(GETやPOSTといったHTTPリクエストメソッドに対する処理を定義必要がある場合)はこのレイヤーの処理を自分で記述することが多い
redirect()
- 別で定義されたurlに飛ばす処理
- あるデータを詳細画面で表示し、それを削除。削除後は一覧画面に飛ばす、みたいな時に利用
- 例えば
urls.py
で下記のように定義されている場合
app_name = 'book'
urlpatterns = [
url(r'books/$', views.books, name='books'),
]
- view内で下記のように利用する
return redirect('book:books') # app_nameとurlpatternのnameから飛ばす先のviewを特定する
- ちなみに
redirect
が返す実態はHttpResponseRedirect
で、下記のように書くことも可能
url = reverse("book:books") # app_nameとurlpatternのnameからurlを特定する
return HttpResponseRedirect(url)
success_url
success_url
というクラス変数に遷移後のURLを定義するやり方DeleteView
利用する場合等
get_success_url()
get_success_url()
という関数をオーバーライドするやり方CreateView
やUpdateView
にあるform_valid()
というフォームで受け取ったデータが正しい場合に使われる関数があり、その内部で呼ばれているのでオーバーライドする- 参考: [Django] success_urlとget_success_urlおよびreverseとreverse_lazyの使い分け
ログインしているユーザーのみアクセスできるView
- 関数ベースビューの場合:
login_required
デコレーターを利用 - クラスベースビューの場合:
LoginRequiredMixin
を利用
Form
- 大きく2種類ある
- Form: 自由に色々記述できる
- ModelForm: 定義済のモデルを利用してFormが作れる
- Formはwidgetを内包する。wigdetがフロントでレンダリングされるHTML構造などを持っている
widgetは自分で作成することもできるが、あまりサンプルが無い。とりあえずフロントで<input>
タグに適当なクラス属性を付与してレンダリングして、その後JSで見た目とか挙動とかを変更できるので、それでやってしまうで大丈夫かと
ModelForm
- とりあえず例を残しておく
from django import forms
from book.models import Review
class ReviewForm(forms.ModelForm):
# フィールドの振る舞いを上書きできる。これはスライダー。フロントではidを基にJSで要素を特定し、挙動を追加している
rating = forms.FloatField(
label='評価', max_value=5, min_value=0,
required=False,
widget=forms.TextInput(
attrs={'type': 'range',
'class': 'slider',
'min': '0',
'max': '5',
'id': 'myRange',
'step': 0.1})
)
comment = forms.CharField(
label='コメント',
required=False,
widget=forms.Textarea(
attrs={'class': 'textarea',
'placeholder': 'とても良い本です'}
)
)
class Meta:
model = Review # 利用するモデルを紐づけ
fields = ('book', 'user', 'rating', 'comment') # そのモデルが持っているフィールドのどれを利用するか定義
Query
- urlを叩いて画面が表示されるまでに時間がかかる場合、SQLが大量に発行されていないか疑う
- Debug Toolbar を見てみて、実際のSQLの本数を見る
- たいていは
select_related()
かprefetch_related()
で解決できる
- N+1問題を解消するもの
- 一対多の関係のデータがあり、多の一覧を表示する時に、それぞれの多と一緒に一の情報も表示したい、という時に多の数分追加でSQLが発行されるという問題
- ↑ は意味わからないはずなので、実例 ↓
-
著者と本が一対多の関係として、本の一覧を表示する時に、それぞれの本と一緒に著者の情報も表示したい、という時に本の数分追加でSQLが発行される
-
より具体的には、
models.py
が下記
class Auther(models.Model):
name = models.CharField(max_length=128)
class Book(models.Model):
author = models.ForeignKey(Auther)
title = models.CharField(max_length=128)
- で、
template
で下記のようにしたい場合
<ul>
{% for book in books %}
<li>{{ book.title }} by {{ book.auther.name}}</li>
{% endfor %}
</ul>
- N+1問題を解消するには、
views.py
でクエリは下記のようにすれば良い
class BookListView(generic.ListView):
model = Book
queryset = Book.objects.select_related('auther')
- 一対多、多対多の関係の時に相手方の多の情報を一緒に表示したい時にクエリ件数を減らす
- 例えば、下記のような状況
- 上記例で著者の情報を表示する時にその著者の本も表示する
- 上記例で本に多対多の関係でタグがつくとして、本を表示する時についているタグも表示する
-
- をコードに落とすと、
views.py
ではクエリを下記のようにする
- をコードに落とすと、
class AutherListView(generic.ListView):
model = Auther
queryset = Auther.objects.prefetch_related('book')
aggregate()
annotate()
- SQLのGROUP BYを実行して集計する
- GROUP BYの対象は
values()
で定義できる - しかし、結果はリストになり、query_setみたいにインスタンスの情報が詰まっててtemplateで自由にそれらの情報を使える!みたいなことにならず、使い勝手が悪い
- 参考: djangoでの集計は辛いという話 — ORMは用法・用量を守って正しく使いましょう
- 参考: Can Django’s .annotate() return objects?
Row SQL
- 生のSQLを発行することができる
- 返り値はリスト
from django.db import connection
...
cursor = connection.cursor()
sql = 'SELECT * FROM "book_book";'
cursor.execute(sql)
return cursor.fetchall()
...
画像ファイルに上限を定める
- 画像をアップロード可能にする場合、画像の上限を設けたい
forms.py
で下記のように記述する
...
def file_size(value):
if value.size > 524288:
raise ValidationError('500キロバイト以下の画像を登録してください。')
class HogeForm(forms.ModelForm):
image = forms.ImageField(
validators=[file_size], # 上記関数を利用
)
...
Pagenator
Template内
- とりあえずコピペで使えるTempkate内の記述
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<!-- 前へ の部分 -->
{% if page_obj.has_previous %}
<a class="pagination-previous has-background-white border-0"
href="?{% url_replace request 'page' page_obj.previous_page_number %}">
<i class="fas fa-angle-left"></i>
</a>
{% endif %}
<ul class="pagination-list">
<!-- 数字の部分 -->
{% for num in page_range %}
{% if page_obj.number == num %}
<a class="pagination-link has-background-info has-text-white border-0" href="#!">{{ num }}</a>
{% else %}
<a class="pagination-link has-background-white border-0"
href="?{% url_replace request 'page' num %}">{{ num }}</a>
{% endif %}
{% endfor %}
</ul>
<!-- 次へ の部分 -->
{% if page_obj.has_next %}
<a class="pagination-next has-background-white border-0"
href="?{% url_replace request 'page' page_obj.next_page_number %}">
<i class="fas fa-angle-right"></i>
</a>
{% endif %}
</nav>
- ↑に加え、↓のhelperの定義を行う
Helperで他のURLパラメーターとページング処理を共生
- ページングは検索画面とかで利用される。そうすると検索用のクエリがURLパラメータとして発生し、これとの齟齬をとりたい
- 具体的には、urlで
?page=2
の部分が変更されても、それ意外のURLパラメータを保持しておきたい - helperを使うと解消できる
helpess.py
を用意し、下記のように記述
from django import template
register = template.Library()
@register.simple_tag
def url_replace(request, field, value):
url_dict = request.GET.copy()
url_dict[field] = str(value)
return url_dict.urlencode()
遷移可能なページの数を限定
- 例えば全部で20ページあり、今5ページ目にいるとして、
< 1 2 3 45
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20>
と表示されるのも長いので、前後3つずつに絞って
< 2 3 45
6 7 8 >
と表示したいとする - 下記のように
page_range
というオブジェクトをtemplateに渡してあげると実現可能
try:
page = int(self.request.GET.get('page', '1'))
except:
page = 1
try:
temp = context['paginator'].page(page)
except(EmptyPage, InvalidPage):
temp = context['paginator'].page(1)
index = temp.number - 1
max_index = len(context['paginator'].page_range)
start_index = index - 2 if index >= 3 else 0
end_index = index + 3 if index <= max_index - 3 else max_index
page_range = list(context['paginator'].page_range)[start_index:end_index]
context['page_range'] = page_range
paginatorインスタンスを自分で作成
- 通常ページ処理は
generic.ListView
を利用することで利用することであまり意識せずに利用可能 - しかし、生のSQLを書いて結果が
query_set
ではない場合などはpagenator
を作ってtemplate
にわたす処理の記述が必要になる - そんなに難しいものでもなく、公式ドキュメントにある
> ペジネータを使うには、まず Paginator クラスにオブジェクトのリスト と、各ページに表示したい要素数を指定してインスタンスを生成します。生成され るインスタンスは、各ページの要素にアクセスするためのメソッドを提供しています
というインストラクション通りにやればできる
>>> from django.core.paginator import Paginator
>>> objects = ['john', 'paul', 'george', 'ringo']
>>> p = Paginator(objects, 2)