iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
佛心分享-IT 人自學之術

It Works on My Machine:30 天從踩雷學會工程事故調查系列 第 18

Day 18|重現:從 Python 原始碼一路追到資料庫真正收到的 SQL

  • 分享至 

  • xImage
  •  

本篇是故事四的「重現」篇。

本篇要回答:一行「等價」的 Python 改寫,為什麼會改變 ORM 查詢的語意?

當時發生了什麼

Day 16 的事故軌跡,完整攤開是七層:

Python 原始碼
  ↓
lint 規則判斷(把這行當成一般布林比較)
  ↓
自動改寫
  ↓
ORM Expression(語意在這裡分岔)
  ↓
SQL 產生
  ↓
資料庫執行
  ↓
查詢結果(合法、無例外、永遠為空)

關鍵在第四層:lint 工具看見的是 Python 語法;ORM 看見的是查詢表達式。同一行字,兩套語意。

我原本怎麼判斷

我原本以為「布林比較怎麼寫都一樣」。這在純 Python 語境大致成立,但 ORM 的欄位物件靠運算子多載(operator overloading)工作——而 Python 的運算子,可多載的範圍是有邊界的。

我怎麼查證或重現

不需要資料庫,一段標準庫就能重現機制(以下皆實際執行驗證):

class Column:                      # 模擬 ORM 欄位的最小示意
    def __init__(self, name):
        self.name = name
    def __eq__(self, other):       # == 可以多載:回傳「查詢條件」
        return f"SQL: {self.name} = {other!r}"

flag = Column("flag")

print(flag == True)   # SQL: flag = True   ← == 被多載成條件物件
print(flag is True)   # False              ← is 不可多載,當場變成純 Python bool
print(bool(flag))     # True               ← 物件的真值判斷,也與欄位值無關

三行輸出說完了整個事故家族的機制:== 這類比較運算子可以被 ORM 攔截,轉成將來送給資料庫的條件;is 與 not 這類運算不可多載,在 Python 層當場求值成一個普通布林——ORM 根本沒有機會參與。一旦某次改寫讓條件在 Python 層先變成了 False 之類的常數,ORM 拿到的就不是「flag 欄位等於真」的條件,而是一個恆假(或恆真)的字面值,查詢自然永遠為空(或永遠全撈)。

(示意聲明:Column 是機制示範,不是當時的程式碼;真實 ORM 的行為以其官方文件為準,但「== 可多載、is 不可多載」是 Python 語言層的事實,適用於所有 ORM。另外提醒:這段示意只多載了 ==、沒有一併定義雜湊方法,實例會因此變成不可雜湊,放進 set 或當成 dict 的鍵會拋 TypeError: unhashable type(已實測)。真實 ORM 會另行處理這件事,這段程式碼只用來觀察機制,不宜直接照抄。)

四個核心問題就位:哪一層第一次改變語意?自動改寫落地、表達式被重新求值的那一刻。工具知道這是 ORM DSL 嗎?不知道,它只看語法。產生的 SQL 與預期一致嗎?對照事故前後的 SQL 是唯一可靠的判準。為什麼沒有例外?因為每一層都在正確執行自己收到的東西——收到的東西本身錯了。

區分證據等級。已確認事實:上述範例輸出與 Python 運算子多載規則。合理推論:當時事故沿同一機制發生。已不可考:當時的確切改寫方向(Day 17)。

今天留下什麼方法

本篇結論:

自動修正事故不能只看 Python 表面,要一路追到 ORM 最後交給資料庫的條件。

下一篇(Day 19)替工具說句公道話,然後把帳算清楚:工具沒有壞,它只是把通用規則套到了不通用的語意上。


上一篇
Day 17|查證:回憶只能當線索,Diff 才是物證
系列文
It Works on My Machine:30 天從踩雷學會工程事故調查18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言