Some more things about Django I've been enjoying

Julia Evans

關於 Django,我最近喜歡上的其他幾個地方

哈囉!我現在正踏上一段有點奇妙的旅程,試著學習用有點像 2010 年代的方式來做網站——有一個 SQL 資料庫,然後在後端渲染 HTML。

這段旅程還滿有趣的,因為用這種方式做網站對我來說不見得感覺「簡單」:我在 2000 年代或 2010 年代從來沒學過怎麼做,有很多東西需要學習。

所以這裡要分享一些 Django 的功能,它們讓打造這類網站感覺比我之前嘗試用 Go 標準函式庫或 Flask 卻屢屢失敗時要可行得多。我也會談談使用 Django 時遇到的幾個問題。

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

之前我做網站時比較有信心的工具組合是:

  • 靜態網站產生器(就像這個部落格用的那種)
  • 會用 JavaScript 做一些有趣功能的靜態網站(像是這個sql playground
  • 使用 Lambda 或 Go 作為後端的簡易 Vue.js 單頁應用程式(像是mess with dns

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

在某種程度上,寫一個盡量少用 JS、以後端為主的網站,對我來說感覺就跟寫一個盡量少做後端、以單頁 JS 網站為主的東西是一樣的,雖然它們看起來像是對立的作法。在這兩種情況下,我都只是想盡量把邏輯集中在同一個地方。

接下來談談我對 Django 的一些想法!

我很喜歡 query builders(查詢建構器)

我在 Django 裡學到,我可以定義一個「query set(查詢集)」類別,裡面有一堆方法,各自帶有我在建構查詢時可能會用到的不同 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

定義篩選條件的語法不是我最喜歡的,但我大部分時間都只是在使用這些方法,感覺非常易讀、很好用,也讓我想去研究其他 query builders 函式庫。以前我總想著「我會 SQL,還需要什麼 query builder?」,但這種結構確實讓程式變得很好讀。

我找到一個有人用 Python 自己寫的小型 query builder的範例,之後想找時間讀讀看,思考一下我是否會喜歡使用更精簡的版本。

template filters(樣板過濾器)超讚

Django 樣板中有許多小巧但能提升使用體驗的可用 filters,對於產生 HTML 超級實用。我目前用過的有:

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

這些單獨來看都是小事,但不知為何,光是能直接使用它們,就感覺帶來很大的不同。

querystring 很酷

我覺得我最喜歡的 template filter 是 querystring:在這個網站上,我們有時會用像 ?date=2026-06-01 這樣的篩選條件來決定顯示什麼內容。querystring 可以產生一個只改動一處的相同 query string 連結,像這樣來連到前一天:

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

或是要移除 outdoors 參數:

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

自動化的 database migrations(資料庫遷移)還是很棒

我還是超愛 Django 的自動資料庫系統。只要編輯一個 model 來新增一個欄位或做其他修改,然後 Django 就會自動產生 migration,這實在太棒了。

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

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

Django 的文件有時會提供使用 class-based views(類別型視圖)與 inheritance 來組織 view 中程式碼的選項。舉例來說,我有四個共享大量程式碼的 views,我可以透過定義某種父類別,再讓其他 views 繼承它,來用 inheritance 管理這些共用邏輯。

我試過之後,發現用 inheritance 在 views 之間共用程式碼的體驗並不怎麼好。我改成使用函式來處理,有點像是這篇文章所提倡的方式,結果就直觀多了。我在 Python 中從來沒有用 inheritance 的好經驗,我想以後也不會再嘗試了。

不過,我並不介意用 inheritance 來使用 Django 本身提供的介面:例如如果我想定義一個 query set,就得寫像 class EventQuerySet(SearchableQuerySetMixin, models.QuerySet) 這樣的東西。我不會想太多,它似乎也能正常運作。

(算是個後設評論:我最近在練習用「某個東西讓我感覺不太好,我比較喜歡另一個東西」的方式來表達我對程式設計的看法。我連結的那篇文章說 function-based views(函式型視圖)是「正確的方式」。我並不是很在意它是不是「正確的」,但知道有其他人對 inheritance 的感受跟我相似,還是讓人感到被肯定。)

我還不太知道該怎麼看待 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 %} 更快」,我在想這差異是否很大,如果是的話又是為什麼

template caching(樣板快取)可能很重要

我想我從 Django 身上學到的一件事是,因為它是個 Framework(框架),所以很容易不小心就設定錯誤。舉例來說,當我剛剛在想為什麼我的網站這麼慢時,我讀了Django 效能文件,注意到裡面有一段話寫道:

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

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

點進連結後,我看到 cached template loader 預設應該是開啟的,但我在嘗試做其他事情時不小心把它關掉了。我想這種「我把預設開啟的 cached template loader 關掉了」的情況,正說明了我還是覺得 Django 的 settings 檔案相當令人困惑、難以理解。我想我之後進去修改時應該要更小心。

在開啟 template caching 之後,網站現在似乎可以相當輕鬆地處理每秒約 12 個請求,而不會佔滿 CPU。我沒有仔細對開啟前後做基準測試,但看起來確實帶來了相當大的差異。

關於 Django 效能,讓我感到意外的一件事是,我一直聽到的建議是「如果有效能問題,就檢查你的資料庫查詢!也許加個索引!」。但我遇到的各種效能問題(像這次 template caching 的事)並不是因為查詢很慢,所以到目前為止,對我來說反而是先跑一次 CPU profile 更有幫助。而且因為我用的是 SQLite,任何緩慢的資料庫查詢問題無論如何都會在 CPU profile 上顯示出來。

總之,我不想太深入探討網站效能。就像我說的,我很容易就會對 profiling 感興趣,但其實我對 profiling 已經了解不少,它也不是我目前最需要學習的東西。

就先到這裡!

之後我可能會再多分享一些關於 Django 讓我喜歡(或讓我感到困擾!)的地方。最近在嘗試寫一些比較短的部落格文章。

原文由 Julia Evans 發布

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