「AI 又不是看不懂 SQL,裸寫 SQL 讓它直接改,不是比多包一層 Repository 介面更快嗎?」
這是我在推動這條規則時最常被問到的問題。乍聽有道理——AI 確實能讀懂一段 SELECT ... WHERE ...,也能照著語法改出一段新的。但「看得懂」跟「能安全改」是兩件完全不同的事。今天要講的,就是為什麼在這套系統裡,任何要碰資料庫的程式碼一律得經過 Repository,不能在呼叫端裸寫 SQL 字串——而且這條規則對 AI 重構的意義,比對人類工程師更關鍵。
先講清楚今天的核心論點:AI 對著一段裸寫 SQL 判斷「這樣改應該安全」的時候,其實有兩件事它窮舉不完,卻常常表現得好像已經窮舉完了。
第一個窮舉不完的問題:這段 SQL 字串(或它的變體)在全站被複製貼上過幾次? Legacy 系統活得越久,同一段查詢邏輯被複製貼上、微調欄位、再複製貼上的機率就越高。AI 改掉眼前這一處,看起來測試綠燈、邏輯正確,但它沒有辦法保證這是唯一一處,也沒辦法保證其他複製出去的版本跟眼前這份完全一致——可能欄位順序不同、可能多了一個條件、可能悄悄修過一次沒人記得。裸寫 SQL 沒有任何機制強迫這些複製品保持同步,AI 只能看見它當下打開的這個檔案。
第二個窮舉不完的問題:這段 SQL 裡的欄位,到底對應哪一條資料庫連線? 在一個有多套子系統並存、資料庫連線分好幾條的架構裡(例如全站共用的一條連線、各租戶各自獨立的一條連線),裸寫 SQL 完全沒辦法從語法上看出「這個查詢應該打到哪一條連線」——這件事只存在於呼叫端當時怎麼拿到 PDO 物件的邏輯裡,往往深藏在好幾層函式呼叫之外。AI 如果只看著 SQL 字串本身判斷「這樣改沒問題」,其實完全沒有觸及這個真正危險的維度。
這正是 Day 01 那句話在資料庫層的具體樣貌:AI 對著一段裸寫 SQL 給出的「已確認安全」,其實只確認了語法正確,沒有確認這段查詢在全站的重複程度、也沒有確認它打的是哪一條連線。
Repository 層的價值,不是「把 SQL 包起來讓程式碼比較好看」這種表面理由,而是把兩類高風險的機械式工作,收斂成一個 AI 只需要呼叫、不需要自己組字串的安全介面:
null 時該產生 IS NULL 還是被跳過)。用一組對照來看這個差異:
❌ 呼叫端裸寫 SQL:
$sql = "SELECT * FROM some_table WHERE status = " . $status
. " AND site_id IN (" . implode(',', $siteIds) . ")";
$stmt = $pdo->query($sql);
→ 手動組字串、手動處理 IN 陣列、$pdo 從哪條連線來的完全看不出來,
改動這段程式碼的人(不管是 AI 還是人類)只能靠讀懂整個呼叫鏈才知道風險在哪
✅ 經過 Repository:
$rows = $repository->findWhere([
'status' => $status,
'site_id' => $siteIds,
]);
→ WHERE 子句、IN 陣列 placeholder 都由 Repository 內部安全組出;
改動的人只需要知道「這支 Repository 對應哪條連線」這一件事,
不需要重新驗證組 SQL 字串的邏輯有沒有漏洞
Repository 把「AI 需要自己判斷安全性」的機械式工作,收斂成「AI 只需要呼叫一個已經驗證過的介面」——這跟 Day 04 講的覆蓋率門檻是同一種思路:不是要求 AI 更謹慎,而是把它需要承擔的風險範圍縮小到一個可以被驗證的邊界。
這裡要補上一層區分:「資料庫存取一律經過 Repository、不在呼叫端裸寫查詢」是一個語言無關的設計原則——Java 生態的 Repository Pattern、C# 的 Repository + Unit of Work、TypeScript 專案裡 ORM 的 Repository 概念,講的都是同一件事:把資料存取邏輯收斂到一層,呼叫端只依賴介面,不直接碰底層查詢語法。
今天範例裡看到的 findWhere() 這種條件陣列 DSL,只是這個 PHP 專案選擇的具體實現方式,換一個專案可能會用 Query Builder、可能會用 ORM 的 fluent interface,形式不同,但「不讓呼叫端自己組查詢字串」這個判斷邏輯是一致的。重點不是記住這支介面長什麼樣子,而是記住:只要你的重構對象有裸寫查詢字串的地方,就值得問一句「這段邏輯有沒有機會收斂成一個介面,讓改動者不用重新驗證組字串的安全性」。
回想你手上的 legacy 系統:有沒有哪段裸寫 SQL(或裸寫查詢語法)你自己也說不清楚它在全站被複製貼上過幾次?如果讓 AI 去改其中一處,你有把握它會意識到還有其他份複製品存在嗎?
明天用一個真實案例把今天的原則落地:一個 Controller 直接查資料庫,重構成經過 Repository 的版本時,實際會踩到哪些意料之外的細節。