Djangoで気に入っていることをもう少し
こんにちは!今、ちょっと面白い挑戦をしています。2010年ごろのスタイルでウェブサイトを作る方法を学ぼうとしているんです。SQLデータベースを用意して、バックエンドでHTMLをレンダリングする、あの感じです。
このやり方は、必ずしも「簡単」とは感じられなくて、そういう意味でも面白い道のりです。2000年代や2010年代にこうした作り方を学んだことがないので、覚えることが山ほどあるんです。
そこで今回は、Goの標準ライブラリやFlaskで試してうまくいかなかったときに比べて、こうしたサイト作りを「自分にもできそう」と思わせてくれたDjangoの機能をいくつか紹介します。あわせて、Djangoでつまずいた点についても触れたいと思います。
なぜ2010年風のウェブサイト作りを学ぶのか
これまで、ウェブサイト作りで自信を持って使えると感じていた道具は次のようなものでした。
- 静的サイトジェネレーター(このブログのように)
- JavaScriptでちょっと面白い動きを加えた静的サイト(たとえばこちらのsql playgroundのような)
- LambdaやGoをバックエンドにした、シンプルなVue.jsのシングルページアプリ(mess with dnsのような)
こうした超シンプルなアプリケーションでは、フロントエンド中心のアプローチがとても気に入っていました。でも、文字どおり1ページだけでなく、たくさんのページを持つものを作ろうと考え始めたとき、フロントエンドのコードが大量に必要になる選択肢にはあまり魅力を感じませんでした。そこでバックエンド側を試してみることにしたんです。
できるだけJSを使わずにバックエンド中心のサイトを書くことは、ある意味、バックエンドの処理をできるだけ少なくしたシングルページのJSサイトを書くことと同じように感じられます。一見正反対に見えますが、どちらの場合も、ロジックをできるだけ一か所にまとめようとしているだけなんです。
それでは、Djangoについて思ったことを書いていきます!
クエリビルダが気に入っています
Djangoでは、クエリを組み立てる際に使いたいWHERE句ごとにメソッドを用意した「クエリセット」クラスを定義できることを知りました。
各メソッドの意味を定義しておくと、ビューではこんなふうに使えます。
Events.objects.approved()
.for_tab(tab)
.with_festivals(tab_params.festival_slugs)
.is_free(tab_params.free)
.is_outdoors(tab_params.outdoors)メソッドの定義はこんな感じです。
class EventQuerySet(SearchableQuerySetMixin, models.QuerySet):
def approved(self):
return self.filter(approved_at__isnull=False)
def future(self):
today = timezone.localdate()
return self.filter(end__gt=self._midnight(today))
def with_tags(self, tags):
if tags:
return self.filter(tags__name__in=tags).distinct()
return selfフィルターを定義する構文はそこまで好みではないのですが、ほとんどの時間は定義したメソッドを使う側で過ごすので、とても読みやすくて使い心地がいいです。今後は他のクエリビルダライブラリも調べてみたくなりました。以前は「SQLは書けるし、クエリビルダなんて要る?」と思っていましたが、こういう構造だと本当に読みやすくなるんです。
自分で小さなクエリビルダをPythonで書いた人の例を見つけたので、あとで読んで、もっとミニマルな形なら自分に合うかどうか考えてみたいと思っています。
テンプレートフィルターが素晴らしい
Djangoのテンプレートには、HTMLを生成するのにとても役立つ、ちょっとした便利なフィルターがたくさん用意されています。これまでに使ったのは次のようなものです。
- プレーンテキストのURLをリンクに変換したり、改行を
<br>に変換したりするもの({{ event.description|urlize|linebreaksbr }}) - 日付のフォーマット(
{{ row.date|date:"M j" }}) json_script。Pythonの辞書を自動でJSONに変換し、安全な形で<script>タグとしてHTMLに埋め込んでくれます
一つひとつは小さなことですが、こうして最初から用意されているだけで、なんだか大きな違いになると感じます。
querystringが便利
お気に入りのテンプレートフィルターはquerystringかもしれません。このサイトでは、表示内容を決めるのに?date=2026-06-01のようなフィルターを使うことがあります。querystringは、同じクエリ文字列を一つだけ変更してリンクを作ってくれます。たとえば前の日付へのリンクはこんなふうに書けます。
<a href="{% querystring date=nav.prev_date%}">あるいはoutdoorsパラメータを取り除くには、こう書きます。
<a href="{% querystring outdoors=None %}">自動のデータベースマイグレーションはやっぱり素晴らしい
Djangoの自動マイグレーションの仕組みは、今でも本当に気に入っています。モデルを編集してフィールドを追加するだけで、Djangoが自動でマイグレーションを生成してくれるのは素晴らしいです。
これまでに19回のデータベースマイグレーションを行いましたが、まだまだ増えそうです。問題への理解が変わるたびに、データベースを気軽に変更できるのは、私にとって大きな違いです。
コードを継承で整理したくありません
Djangoのドキュメントでは、ビューのコードを整理する方法として、クラスベースビューと継承を使う選択肢が示されることがあります。たとえば私の場合は、共通部分の多いビューが4つあり、親クラスを定義して他のビューに継承させることで管理することもできました。
試してみたのですが、継承でビュー間のコードを共有するのはあまり心地よくありませんでした。こちらの記事が勧めているようなやり方で、関数を使う形に切り替えたところ、ずっとシンプルになりました。Pythonで継承を使って良い思いをしたことがないので、もう試すことはないと思います。
ただ、Django自体が提供するインターフェースを使うための継承は気になりません。たとえばクエリセットを定義したいときは、class EventQuerySet(SearchableQuerySetMixin, models.QuerySet)のように書く必要がありますが、深く考えずに書いてもちゃんと動いてくれます。
(余談ですが、プログラミングについての意見を言うときは、「〇〇は自分にはしっくりこなくて、△△のほうが好きです」という言い方を心がけています。リンク先の記事では関数ベースビューが「正しいやり方」とされていますが、それが「正しい」かどうかにはあまりこだわりはありません。ただ、継承について自分と同じように感じている人がいると知れて、なんだか安心しました)
Djangoのパフォーマンスをどう考えればいいのか、まだわかりません
あるとき、LLMのスクレイパーにサイトを見つけられ、1秒あたり10リクエストほど送られてくるようになりました。今のところブロックできていて問題はありませんが、サイトがどれくらいの負荷に耐えられるのかを考えるきっかけになりました。Goでバックエンドを書くときは、パフォーマンスの見通しがかなりシンプルで(たいていは十分速い)、Djangoのサイトはそれとはまったく違うと感じています。
軽く負荷テスト(ab -n 1000 -c 1)をしてみたところ、今のところ1秒あたり2〜3リクエストくらいを捌ける程度でした(月10ドルほどのVMで)。
遅い部分を突き止めて速くしようと、プロファイリングに没頭したくなる気持ちもあります(py-spyというツールがあって、とても手軽で優秀ですし、プロファイリング自体楽しいんです!)。ただ、Djangoサイトにどれくらいのパフォーマンスを期待すべきなのか、もっと高い視点でどう考えるべきなのかが、正直まだよくわかっていません。
まだ整理できていない疑問をいくつか挙げると、
- たまにトラフィックが急増するサイトの場合、スケールアップできるようにしておくべきでしょうか
- もっと多くのものをキャッシュできるように設計すべきでしょうか(というか、本当にやらなきゃいけないのでしょうか?キャッシュを正しく扱うのは本当に面倒なのに!)
- Djangoのパフォーマンスに関するドキュメントでは、テンプレートにはJinjaのほうが速いと書かれていますが、テンプレートシステムを切り替えることを考えるべきでしょうか
- 同じドキュメントには「{% block %}は{% include %}より速い」とも書かれていますが、どれくらい違うのか、もし違うならなぜなのか気になっています
テンプレートのキャッシュは重要かもしれません
Djangoについて学んでいることの一つは、Djangoがフレームワークであるがゆえに、うっかり設定を間違えやすいということです。たとえば今回サイトが遅い理由を考えていたとき、Djangoのパフォーマンスに関するドキュメントを読んで、こんな記述を見つけました。
キャッシュされるテンプレートローダーを有効にすると、テンプレートをレンダリングするたびにコンパイルする手間が省けるため、多くの場合パフォーマンスが大幅に向上します。
CPUプロファイリングをしたときに、テンプレートのレンダリングにかなり時間がかかっていることに気づいていたので、これは役に立つかもしれないと思いました!
リンク先を見てみると、キャッシュされるテンプレートローダーはデフォルトで有効になっているはずなのに、別のことをしようとして誤って無効にしていました。この「デフォルトで有効なはずのテンプレートローダーを自分で無効にしてしまっていた」というのは、Djangoの設定ファイルがいまだにわかりにくく難しいと感じている一例だと思います。触るときは気をつけないといけませんね。
テンプレートのキャッシュを有効にしたところ、CPUを使い切ることなく、1秒あたり12リクエスト程度は余裕で捌けるようになったようです。厳密に前後を比較したわけではありませんが、かなり大きな違いがあったように感じます。
Djangoのパフォーマンスについて驚いたことの一つは、これまで「パフォーマンスに問題があるならデータベースクエリを見直そう!インデックスを追加してみよう!」というアドバイスをよく聞いてきたことです。でも実際に出くわしたパフォーマンスの問題(今回のテンプレートキャッシュのようなもの)は、遅いクエリが原因ではないものがいろいろあり、むしろ最初にCPUプロファイルを取るほうがこれまでのところ役に立っています。しかもSQLiteを使っているので、もしクエリが遅ければそれもCPUプロファイルに現れるはずです。
いずれにせよ、サイトのパフォーマンスの話に深入りしすぎないようにしたいと思います。先ほども言ったように、プロファイリングにはつい夢中になってしまいますが、プロファイリングについてはすでにそれなりに知っているので、今いちばん学ぶべきことではないんです。
今日はここまで!
また今度、Djangoで気に入っていること(あるいは苦労していること!)について、続きを書くかもしれません。最近は短めのブログ記事を書くようにしています。
記事をランダムに読む