Some more things about Django I've been enjoying

Julia Evans

再來分享幾個我最近很喜歡的 Django 功能

原文由 Julia Evans 發布,訂閱此部落格

哈囉!我最近踏上了一段有點好笑的旅程,試著學習用一種有點像 2010 年代風格的方式來做網站——有個 SQL 資料庫,然後在後端把 HTML 渲染出來。

這趟旅程還滿有趣的,因為對我來說,用這種方式做網站並不一定覺得「簡單」:我在 2000 年代或 2010 年代根本沒學過怎麼做,中間有很多東西要學。

所以我想來分享幾個 Django 的功能,它們讓我覺得做這類網站比起之前用 Go 標準函式庫或 Flask 屢屢失敗的經驗,要來得可行許多。我也會聊一下遇到的幾個 Django 問題。

為什麼要學得像 2010 年那樣做網站?

之前我做網站時,比較有把握的工具組合是:

  • 靜態網站產生器(就像這個部落格用的那種)
  • 會用 JavaScript 玩點有趣花樣的靜態網站(像是這個 sql playground
  • 簡單的 Vue.js 單頁應用,後端用 Lambda 或 Go(像是 mess with dns

我真的很喜歡這種以「前端為主」的做法來做這些超簡單的應用程式,但當我開始想做一個有很多不同頁面的東西(而不是真的就只有一頁)時,對於那些需要寫一堆前端程式碼的選項,我就沒那麼興奮了。所以我想,不如來試試後端。

對我來說,寫一個盡量少用 JS、以後端為主的網站,感覺上跟寫一個盡量少寫後端邏輯的單頁 JS 網站是一樣的,雖然它們看起來像是相反的兩端。但不管是哪一種,我都只是想把大部分的邏輯盡量集中在同一個地方。

接下來就來聊聊 Django 吧!

我很喜歡 query builder

我學到可以在 Django 裡定義一個「query set」類別,裡面放一堆方法,各自對應到組 query 時可能會用到的不同 WHERE 條件:

在我把這些方法的意義都定義好之後,在 view 裡面是這樣用的:

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

定義這些 filter 的語法不是我最喜歡的,但我大部分時間都只是在呼叫這些方法,用起來非常好讀、很好用,也讓我想之後去研究其他的 query builder 函式庫。以前我總覺得「我會 SQL,還需要什麼 query builder?」,但這種結構真的讓程式碼變得很好讀。

我找到一個範例,有人在 Python 裡自己用 Python 寫了一個小型的 query builder,我想之後找時間仔細讀一下,看看自己會不會喜歡用更精簡的版本。

樣板 filter 超棒

在 Django 樣板裡有一堆現成的樣板 filter,對產生 HTML 超級實用。我目前用過的有:

  • 把純文字的 URL 轉成連結,或把換行轉成 <br>{{ event.description|urlize|linebreaksbr }}
  • 格式化日期({{ row.date|date:"M j" }}
  • json_script,它會把 Python 的 dictionary 自動轉成 JSON,並以安全的方式當作 <script> 標籤插入到 HTML 裡

每一項單獨看都是小東西,但不知為何,光是這些東西現成可用,就感覺差很多。

querystring 很酷

我覺得我最喜歡的樣板 filter 是 querystring:在這個網站上,我們有時會用像 ?date=2026-06-01 這樣的 filter 來決定要顯示什麼。querystring 可以幫你產生一個只改動一個參數的同樣 query string 連結,例如這樣來連到前一天:

<a href="{% querystring date=nav.prev_date%}">

或是要移除 outdoors 參數:

<a href="{% querystring outdoors=None %}">

自動化的資料庫 migration 還是很棒

我還是超愛 Django 的自動資料庫系統。只要改一下 model,加個新欄位什麼的,Django 就會自動幫你產生 migration,真的很神奇。

到目前為止我們已經做了 19 次資料庫 migration,我想之後應該還會有更多!能夠隨著我對問題的理解改變,輕鬆地修改資料庫,對我來說幫助非常大。

我不想用繼承來組織程式碼

Django 的文件有時會建議用 class-based view 和繼承來組織 view 裡的程式碼。舉例來說,我有四個 view 共享了很多程式碼,我本來可以用繼承來管理,定義一個父類別,然後讓其他 view 去繼承它。

我試過了,但我一點也不喜歡用繼承在 view 之間共用程式碼的體驗。我後來改用函式,就像這篇文章提倡的那樣,整個就直觀多了。我在 Python 裡用繼承從來沒有過什麼好經驗,我想我以後大概不會再嘗試了。

不過,對於 Django 本身提供的介面要用繼承,我倒是不介意:例如,如果我想定義一個 query set,就得寫成像 class EventQuerySet(SearchableQuerySetMixin, models.QuerySet) 這樣。我不太去細想它,感覺也能正常運作。

(順帶一提:我最近在練習用「某件事讓我感覺不太好,我比較喜歡另一件事」的方式來表達我對程式寫法的看法。我連結的那篇文章說 function-based view 才是「正確的方式」。我其實不太在乎它是不是真的「正確」,但知道有其他人對繼承也有類似的感覺,還是讓人感到安慰。)

我還不太知道該怎麼看待 Django 的效能

有段時間,LLM 爬蟲發現了我們的網站,開始以每秒大概 10 個請求的速度打過來。我把它們擋掉了,目前還算有效,但這讓我開始思考網站的承載能力到底有多少。我習慣寫 Go 的後端,那邊的效能狀況相對單純(通常就是夠快),而 Django 網站就很不一樣。

做了一些簡單的負載測試(用 ab -n 1000 -c 1),顯示我們目前大約每秒可以處理 2 到 3 個請求(在一台大約每月 10 美元的 VM 上)。

我很想一頭栽進去做一堆 profiling,找出哪裡慢然後想辦法把它變快(有 py-spy 可以用,而且 py-spy 超棒、超好用,profiling 本身也很有趣!)但我真的不太清楚對於一個 Django 網站,我該期待怎樣的效能,又該從更高的層次怎麼去思考這件事。

有些事我還沒搞懂:

  • 如果我的網站偶爾會遇到瞬間爆量的流量,我會需要能夠擴展嗎?
  • 我該把網站設計成更多東西可以被快取嗎?(而且我真的得這麼做嗎?快取要弄對真的很煩!)
  • Django 效能文件說用 Jinja 做樣板比較快,我該考慮換樣板系統嗎?
  • 那些文件還說「{% block %} 比用 {% include %} 快」,我在想這差異會很大嗎?如果會,又是為什麼

樣板快取可能很重要

我覺得我在學 Django 的過程中領悟到的一件事是,正因為它是個 Framework(tm),很容易就不小心把它設定錯。舉例來說,前陣子我在想為什麼網站這麼慢時,去讀了 Django 效能文件,注意到一段話寫道:

啟用快取的樣板載入器通常能大幅提升效能,因為它避免了每次需要渲染樣板時都要重新編譯。

當時我做 CPU profiling 就有注意到,它花了很多時間在渲染樣板上!也許這個可以幫上忙!

點進那個連結後,我發現快取樣板載入器本來應該是預設開啟的,但我在嘗試做別的事情時不小心把它關掉了。我覺得這種「不小心把預設的快取樣板載入器關掉」的事,正好說明了我還是覺得 Django 的設定檔很讓人困惑、很難搞。我想之後進去改的時候應該要更小心一點。

開啟樣板快取之後,網站現在好像可以相當輕鬆地處理每秒大約 12 個請求,而且還不會把 CPU 吃滿。我沒有很仔細地對比開啟前後的數據,但感覺差異還滿大的。

關於 Django 效能,讓我有點意外的一件事是,我以前一直聽到的建議都是「如果有效能問題,就去檢查資料庫查詢!也許加個索引!」。但我遇到的各種效能問題(像這次的樣板快取)都不是因為查詢太慢,所以到目前為止,對我來說反而是先跑一下 CPU profile 比較有用。而且因為我用的是 SQLite,任何慢查詢的問題本來就會出現在 CPU profile 上。

總之,我不想太深入鑽研網站效能。就像我說的,我很容易就會對 profiling 產生興趣,但其實我對 profiling 已經算懂不少了,這也不是我現在最需要學的東西。

今天就先到這裡!

之後也許會再多聊聊我喜歡(或覺得卡關的!)Django 的地方。最近在嘗試寫一些短一點的部落格文章。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言