Some more things about Django I've been enjoying

Julia Evans

Djangoで最近気に入っていることをもう少し

原文は Julia Evans により に公開されました。 このブログを購読する

こんにちは!今、ちょっと面白い挑戦をしていて、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生成にとても役立つ、ちょっとしたQOL向上系のフィルターがたくさん用意されています。これまでに使ったものは以下のとおりです。

  • プレーンテキストのURLをリンクにしたり、改行を<br>に変換したりするもの({{ event.description|urlize|linebreaksbr }}
  • 日付のフォーマット({{ row.date|date:"M j" }}
  • json_script。Pythonの辞書を自動的にJSONに変換し、安全な形で<script>タグとしてHTMLに埋め込んでくれます

どれも一つひとつは小さなことですが、こうして最初から用意されているというだけで、なんだか大きな違いを感じます。

querystringが便利

私のお気に入りのテンプレートフィルターはquerystringだと思います。このサイトでは、表示内容を決めるのに?date=2026-06-01のようなフィルターを使うことがあります。querystringは、同じクエリ文字列に対して1つだけ変更を加えたリンクを作ってくれます。例えば、前の日付へのリンクはこんなふうに書けます。

<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)のように書く必要がありますが、あまり深く考えずに書いても、ちゃんと動いてくれます。

(ちょっとメタな話ですが、最近はプログラミングについての意見を語るときは、単に「THINGは自分にはしっくりこない、代わりにOTHER THINGの方が好きだ」と言うようにしています。リンクした記事では関数ベースビューが「正しいやり方」だと書かれていますが、それが本当に「正しい」かどうかにはあまりこだわっていません。ただ、継承について自分と同じように感じている人が他にもいると知れて、なんだか安心しました)

Djangoのパフォーマンスをどう考えればいいのか分からない

あるとき、LLMのスクレイパーにサイトを見つけられて、1秒あたり10リクエストくらい送られてくるようになりました。今のところはブロックして対処できていますが、サイトの処理能力はどれくらいなんだろうと考えるきっかけになりました。Goでバックエンドを書くことに慣れていると、パフォーマンスの状況はかなりシンプルで(たいていはすべてが十分に速い)、Djangoサイトはそれとはまったく違います。

軽く負荷テスト(ab -n 1000 -c 1で)をしてみたところ、今のところ1秒あたり2〜3リクエストくらいを捌けるようです(月10ドル程度のVM上で)。

どこが遅いのかを突き止めるためにプロファイリングをあれこれやって、速くしようとする沼にハマりたくなります(そのためのpy-spyというツールもあります。py-spyは素晴らしくて超簡単に使えて、プロファイリング自体も楽しいんです!)。でも、Djangoサイトにどれくらいのパフォーマンスを期待すべきなのか、もっと高い視点でどう考えるべきなのかが、本当に分からないのです。

まだ自分の中で答えが出ていないことはこんな感じです。

  • たまにトラフィックが急増するようなサイトの場合、スケールアップできるようにしておくべきか?
  • もっと多くのものをキャッシュできるようにサイトを設計すべきか?(そして、本当にそうしなければいけないのか?キャッシュを正しく扱うのは本当に面倒!)
  • Djangoのパフォーマンスに関するドキュメントには、テンプレートにはJinjaの方が速いと書かれているけど、テンプレートシステムを切り替えることを考えるべきか?
  • そのドキュメントには「{% block %}は{% include %}を使うより速い」とも書かれているけど、それは大きな違いなのか、もしそうならなぜなのか気になる

テンプレートのキャッシュは重要かもしれない

Djangoについて学んでいることの一つは、Djangoが(いわゆる)フレームワークであるがゆえに、うっかり設定を間違えやすいということだと思います。例えば、なぜサイトが遅いのか考えていたときにDjangoのパフォーマンスに関するドキュメントを読んで、こんな記述に気づきました。

キャッシュされたテンプレートローダーを有効にすると、テンプレートをレンダリングするたびにコンパイルするのを避けられるため、パフォーマンスが劇的に向上することがよくあります。

CPUプロファイリングをしたとき、テンプレートのレンダリングにかなり時間がかかっていることに気づいていました!これは役に立つかもしれない!

リンクをたどってみると、キャッシュされたテンプレートローダーはデフォルトでオンになっているはずなのに、他のことをしようとしている間にうっかりオフにしてしまっていたことが分かりました。この「デフォルトでオンのはずのキャッシュされたテンプレートローダーをオフにしてしまった」というのは、Djangoの設定ファイルがいまだにかなり分かりにくく難しいと感じていることのいい例だと思います。あそこを触るときは、もっと気をつけないといけませんね。

テンプレートのキャッシュをオンにしたあとは、CPUを使い切ることなく、1秒あたり12リクエスト程度ならかなり楽に捌けるようになったようです。ビフォーアフターを厳密にベンチマークしたわけではありませんが、かなり大きな違いがあったように思います。

Djangoのパフォーマンスについて驚いたことの一つは、これまでずっと「パフォーマンスに問題があるなら、データベースクエリをチェックしろ!インデックスを追加してみろ!」というアドバイスを聞いてきたことです。でも、実際に遭遇してきたさまざまなパフォーマンス問題(今回のテンプレートキャッシュのようなもの)は、遅いクエリが原因ではなかったので、むしろ最初にCPUプロファイルを取ってみる方がこれまでのところ役に立っています。それに、SQLiteを使っているので、もし遅いデータベースクエリがあったとしても、結局はCPUプロファイルに現れるはずですし。

まあ、サイトのパフォーマンスの話に深入りしすぎるつもりはありません。言ったように、プロファイリングには簡単に夢中になってしまうのですが、実際のところプロファイリングについてはすでにかなり知っているので、今学ぶべき最も重要なことではないのです。

とりあえず今回はここまで!

Djangoについて気に入っていること(あるいは苦労していること!)は、また後で書くかもしれません。最近は短めのブログ記事を書くようにしています。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント